Skip to content

Commit 9751d4d

Browse files
authored
Allow the SceneBuilder, PictureRecord, and Canvas constructor calls from the rendering layer to be hooked (#147271)
This also includes some minor cleanup of documentation, asserts, and tests.
1 parent c22ed98 commit 9751d4d

File tree

7 files changed

+213
-25
lines changed

7 files changed

+213
-25
lines changed

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import 'dart:ui' as ui show SemanticsUpdate;
5+
import 'dart:ui' as ui show PictureRecorder, SceneBuilder, SemanticsUpdate;
66

77
import 'package:flutter/foundation.dart';
88
import 'package:flutter/gestures.dart';
@@ -350,6 +350,31 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
350350
return ViewConfiguration.fromView(renderView.flutterView);
351351
}
352352

353+
/// Create a [SceneBuilder].
354+
///
355+
/// This hook enables test bindings to instrument the rendering layer.
356+
///
357+
/// This is used by the [RenderView] to create the [SceneBuilder] that is
358+
/// passed to the [Layer] system to render the scene.
359+
ui.SceneBuilder createSceneBuilder() => ui.SceneBuilder();
360+
361+
/// Create a [PictureRecorder].
362+
///
363+
/// This hook enables test bindings to instrument the rendering layer.
364+
///
365+
/// This is used by the [PaintingContext] to create the [PictureRecorder]s
366+
/// used when painting [RenderObject]s into [Picture]s passed to
367+
/// [PictureLayer]s.
368+
ui.PictureRecorder createPictureRecorder() => ui.PictureRecorder();
369+
370+
/// Create a [Canvas] from a [PictureRecorder].
371+
///
372+
/// This hook enables test bindings to instrument the rendering layer.
373+
///
374+
/// This is used by the [PaintingContext] after creating a [PictureRecorder]
375+
/// using [createPictureRecorder].
376+
Canvas createCanvas(ui.PictureRecorder recorder) => Canvas(recorder);
377+
353378
/// Called when the system metrics change.
354379
///
355380
/// See [dart:ui.PlatformDispatcher.onMetricsChanged].

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,10 @@ const String _flutterRenderingLibrary = 'package:flutter/rendering.dart';
8585
/// different parents. The scene must be explicitly recomposited after such
8686
/// changes are made; the layer tree does not maintain its own dirty state.
8787
///
88-
/// To composite the tree, create a [SceneBuilder] object, pass it to the
89-
/// root [Layer] object's [addToScene] method, and then call
90-
/// [SceneBuilder.build] to obtain a [Scene]. A [Scene] can then be painted
91-
/// using [dart:ui.FlutterView.render].
88+
/// To composite the tree, create a [SceneBuilder] object using
89+
/// [RendererBinding.createSceneBuilder], pass it to the root [Layer] object's
90+
/// [addToScene] method, and then call [SceneBuilder.build] to obtain a [Scene].
91+
/// A [Scene] can then be painted using [dart:ui.FlutterView.render].
9292
///
9393
/// ## Memory
9494
///
@@ -765,6 +765,8 @@ abstract class Layer with DiagnosticableTreeMixin {
765765
/// layer in [RenderObject.paint], it should dispose of the handle to the
766766
/// old layer. It should also dispose of any layer handles it holds in
767767
/// [RenderObject.dispose].
768+
///
769+
/// To dispose of a layer handle, set its [layer] property to null.
768770
class LayerHandle<T extends Layer> {
769771
/// Create a new layer handle, optionally referencing a [Layer].
770772
LayerHandle([this._layer]) {

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:flutter/painting.dart';
1111
import 'package:flutter/scheduler.dart';
1212
import 'package:flutter/semantics.dart';
1313

14+
import 'binding.dart';
1415
import 'debug.dart';
1516
import 'layer.dart';
1617

@@ -331,8 +332,8 @@ class PaintingContext extends ClipContext {
331332
void _startRecording() {
332333
assert(!_isRecording);
333334
_currentLayer = PictureLayer(estimatedBounds);
334-
_recorder = ui.PictureRecorder();
335-
_canvas = Canvas(_recorder!);
335+
_recorder = RendererBinding.instance.createPictureRecorder();
336+
_canvas = RendererBinding.instance.createCanvas(_recorder!);
336337
_containerLayer.append(_currentLayer!);
337338
}
338339

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

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@ class ViewConfiguration {
6666
return Matrix4.diagonal3Values(devicePixelRatio, devicePixelRatio, 1.0);
6767
}
6868

69+
/// Returns whether [toMatrix] would return a different value for this
70+
/// configuration than it would for the given `oldConfiguration`.
71+
bool shouldUpdateMatrix(ViewConfiguration oldConfiguration) {
72+
if (oldConfiguration.runtimeType != runtimeType) {
73+
// New configuration could have different logic, so we don't know
74+
// whether it will need a new transform. Return a conservative result.
75+
return true;
76+
}
77+
// For this class, the only input to toMatrix is the device pixel ratio,
78+
// so we return true if they differ and false otherwise.
79+
return oldConfiguration.devicePixelRatio != devicePixelRatio;
80+
}
81+
6982
/// Transforms the provided [Size] in logical pixels to physical pixels.
7083
///
7184
/// The [FlutterView.render] method accepts only sizes in physical pixels, but
@@ -103,6 +116,16 @@ class ViewConfiguration {
103116
/// The view represents the total output surface of the render tree and handles
104117
/// bootstrapping the rendering pipeline. The view has a unique child
105118
/// [RenderBox], which is required to fill the entire output surface.
119+
///
120+
/// This object must be bootstrapped in a specific order:
121+
///
122+
/// 1. First, set the [configuration] (either in the constructor or after
123+
/// construction).
124+
/// 2. Second, [attach] the object to a [PipelineOwner].
125+
/// 3. Third, use [prepareInitialFrame] to bootstrap the layout and paint logic.
126+
///
127+
/// After the bootstrapping is complete, the [compositeFrame] method may be used
128+
/// to obtain the rendered output.
106129
class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> {
107130
/// Creates the root of the render tree.
108131
///
@@ -140,6 +163,9 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
140163
/// [TestFlutterView.physicalSize] on the appropriate [TestFlutterView]
141164
/// (typically [WidgetTester.view]) instead of setting a configuration
142165
/// directly on the [RenderView].
166+
///
167+
/// A [configuration] must be set (either directly or by passing one to the
168+
/// constructor) before calling [prepareInitialFrame].
143169
ViewConfiguration get configuration => _configuration!;
144170
ViewConfiguration? _configuration;
145171
set configuration(ViewConfiguration value) {
@@ -149,17 +175,19 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
149175
final ViewConfiguration? oldConfiguration = _configuration;
150176
_configuration = value;
151177
if (_rootTransform == null) {
152-
// [prepareInitialFrame] has not been called yet, nothing to do for now.
178+
// [prepareInitialFrame] has not been called yet, nothing more to do for now.
153179
return;
154180
}
155-
if (oldConfiguration?.toMatrix() != configuration.toMatrix()) {
181+
if (oldConfiguration == null || configuration.shouldUpdateMatrix(oldConfiguration)) {
156182
replaceRootLayer(_updateMatricesAndCreateNewRootLayer());
157183
}
158184
assert(_rootTransform != null);
159185
markNeedsLayout();
160186
}
161187

162188
/// Whether a [configuration] has been set.
189+
///
190+
/// This must be true before calling [prepareInitialFrame].
163191
bool get hasConfiguration => _configuration != null;
164192

165193
@override
@@ -202,15 +230,23 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
202230

203231
/// Bootstrap the rendering pipeline by preparing the first frame.
204232
///
205-
/// This should only be called once, and must be called before changing
206-
/// [configuration]. It is typically called immediately after calling the
207-
/// constructor.
233+
/// This should only be called once. It is typically called immediately after
234+
/// setting the [configuration] the first time (whether by passing one to the
235+
/// constructor, or setting it directly). The [configuration] must have been
236+
/// set before calling this method, and the [RenderView] must have been
237+
/// attached to a [PipelineOwner] using [attach].
208238
///
209239
/// This does not actually schedule the first frame. Call
210-
/// [PipelineOwner.requestVisualUpdate] on [owner] to do that.
240+
/// [PipelineOwner.requestVisualUpdate] on the [owner] to do that.
241+
///
242+
/// This should be called before using any methods that rely on the [layer]
243+
/// being initialized, such as [compositeFrame].
244+
///
245+
/// This method calls [scheduleInitialLayout] and [scheduleInitialPaint].
211246
void prepareInitialFrame() {
212-
assert(owner != null);
213-
assert(_rootTransform == null);
247+
assert(owner != null, 'attach the RenderView to a PipelineOwner before calling prepareInitialFrame');
248+
assert(_rootTransform == null, 'prepareInitialFrame must only be called once'); // set by _updateMatricesAndCreateNewRootLayer
249+
assert(hasConfiguration, 'set a configuration before calling prepareInitialFrame');
214250
scheduleInitialLayout();
215251
scheduleInitialPaint(_updateMatricesAndCreateNewRootLayer());
216252
assert(_rootTransform != null);
@@ -219,6 +255,7 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
219255
Matrix4? _rootTransform;
220256

221257
TransformLayer _updateMatricesAndCreateNewRootLayer() {
258+
assert(hasConfiguration);
222259
_rootTransform = configuration.toMatrix();
223260
final TransformLayer rootLayer = TransformLayer(transform: _rootTransform);
224261
rootLayer.attach(this);
@@ -295,12 +332,19 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
295332
/// Uploads the composited layer tree to the engine.
296333
///
297334
/// Actually causes the output of the rendering pipeline to appear on screen.
335+
///
336+
/// Before calling this method, the [owner] must be set by calling [attach],
337+
/// the [configuration] must be set to a non-null value, and the
338+
/// [prepareInitialFrame] method must have been called.
298339
void compositeFrame() {
299340
if (!kReleaseMode) {
300341
FlutterTimeline.startSync('COMPOSITING');
301342
}
302343
try {
303-
final ui.SceneBuilder builder = ui.SceneBuilder();
344+
assert(hasConfiguration, 'set the RenderView configuration before calling compositeFrame');
345+
assert(_rootTransform != null, 'call prepareInitialFrame before calling compositeFrame');
346+
assert(layer != null, 'call prepareInitialFrame before calling compositeFrame');
347+
final ui.SceneBuilder builder = RendererBinding.instance.createSceneBuilder();
304348
final ui.Scene scene = layer!.buildScene(builder);
305349
if (automaticSystemUiAdjustment) {
306350
_updateSystemChrome();
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:ui' as ui;
6+
7+
import 'package:flutter/foundation.dart';
8+
import 'package:flutter/gestures.dart';
9+
import 'package:flutter/rendering.dart';
10+
import 'package:flutter/scheduler.dart';
11+
import 'package:flutter/services.dart';
12+
import 'package:flutter_test/flutter_test.dart';
13+
14+
final List<String> log = <String>[];
15+
16+
void main() {
17+
final PaintingMocksTestRenderingFlutterBinding binding = PaintingMocksTestRenderingFlutterBinding.ensureInitialized();
18+
19+
test('createSceneBuilder et al', () async {
20+
final RenderView root = RenderView(
21+
view: binding.platformDispatcher.views.single,
22+
configuration: const ViewConfiguration(),
23+
);
24+
root.attach(PipelineOwner());
25+
root.prepareInitialFrame();
26+
expect(log, isEmpty);
27+
root.compositeFrame();
28+
expect(log, <String>['createSceneBuilder']);
29+
log.clear();
30+
final PaintingContext context = PaintingContext(ContainerLayer(), Rect.zero);
31+
expect(log, isEmpty);
32+
context.canvas;
33+
expect(log, <String>['createPictureRecorder', 'createCanvas']);
34+
log.clear();
35+
context.addLayer(ContainerLayer());
36+
expect(log, isEmpty);
37+
context.canvas;
38+
expect(log, <String>['createPictureRecorder', 'createCanvas']);
39+
log.clear();
40+
});
41+
}
42+
43+
44+
class PaintingMocksTestRenderingFlutterBinding extends BindingBase with SchedulerBinding, ServicesBinding, GestureBinding, PaintingBinding, SemanticsBinding, RendererBinding {
45+
@override
46+
void initInstances() {
47+
super.initInstances();
48+
_instance = this;
49+
}
50+
51+
static PaintingMocksTestRenderingFlutterBinding get instance => BindingBase.checkInstance(_instance);
52+
static PaintingMocksTestRenderingFlutterBinding? _instance;
53+
54+
static PaintingMocksTestRenderingFlutterBinding ensureInitialized() {
55+
if (PaintingMocksTestRenderingFlutterBinding._instance == null) {
56+
PaintingMocksTestRenderingFlutterBinding();
57+
}
58+
return PaintingMocksTestRenderingFlutterBinding.instance;
59+
}
60+
61+
@override
62+
ui.SceneBuilder createSceneBuilder() {
63+
log.add('createSceneBuilder');
64+
return super.createSceneBuilder();
65+
}
66+
67+
@override
68+
ui.PictureRecorder createPictureRecorder() {
69+
log.add('createPictureRecorder');
70+
return super.createPictureRecorder();
71+
}
72+
73+
@override
74+
Canvas createCanvas(ui.PictureRecorder recorder) {
75+
log.add('createCanvas');
76+
return super.createCanvas(recorder);
77+
}
78+
}

packages/flutter_test/lib/src/binding.dart

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2098,7 +2098,11 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
20982098
///
20992099
/// The resulting ViewConfiguration maps the given size onto the actual display
21002100
/// using the [BoxFit.contain] algorithm.
2101-
class TestViewConfiguration extends ViewConfiguration {
2101+
///
2102+
/// If the underlying [FlutterView] changes, a new [TestViewConfiguration] should
2103+
/// be created. See [RendererBinding.handleMetricsChanged] and
2104+
/// [RendererBinding.createViewConfigurationFor].
2105+
class TestViewConfiguration implements ViewConfiguration {
21022106
/// Deprecated. Will be removed in a future version of Flutter.
21032107
///
21042108
/// This property has been deprecated to prepare for Flutter's upcoming
@@ -2120,14 +2124,29 @@ class TestViewConfiguration extends ViewConfiguration {
21202124
/// Creates a [TestViewConfiguration] with the given size and view.
21212125
///
21222126
/// The [size] defaults to 800x600.
2123-
TestViewConfiguration.fromView({required ui.FlutterView view, Size size = _kDefaultTestViewportSize})
2124-
: _paintMatrix = _getMatrix(size, view.devicePixelRatio, view),
2125-
_physicalSize = view.physicalSize,
2126-
super(
2127-
devicePixelRatio: view.devicePixelRatio,
2128-
logicalConstraints: BoxConstraints.tight(size),
2129-
physicalConstraints: BoxConstraints.tight(size) * view.devicePixelRatio,
2130-
);
2127+
///
2128+
/// The settings of the given [FlutterView] are captured when the constructor
2129+
/// is called, and subsequent changes are ignored. A new
2130+
/// [TestViewConfiguration] should be created if the underlying [FlutterView]
2131+
/// changes. See [RendererBinding.handleMetricsChanged] and
2132+
/// [RendererBinding.createViewConfigurationFor].
2133+
TestViewConfiguration.fromView({
2134+
required ui.FlutterView view,
2135+
Size size = _kDefaultTestViewportSize,
2136+
}) : devicePixelRatio = view.devicePixelRatio,
2137+
logicalConstraints = BoxConstraints.tight(size),
2138+
physicalConstraints = BoxConstraints.tight(size) * view.devicePixelRatio,
2139+
_paintMatrix = _getMatrix(size, view.devicePixelRatio, view),
2140+
_physicalSize = view.physicalSize;
2141+
2142+
@override
2143+
final double devicePixelRatio;
2144+
2145+
@override
2146+
final BoxConstraints logicalConstraints;
2147+
2148+
@override
2149+
final BoxConstraints physicalConstraints;
21312150

21322151
static Matrix4 _getMatrix(Size size, double devicePixelRatio, ui.FlutterView window) {
21332152
final double inverseRatio = devicePixelRatio / window.devicePixelRatio;
@@ -2158,6 +2177,18 @@ class TestViewConfiguration extends ViewConfiguration {
21582177
@override
21592178
Matrix4 toMatrix() => _paintMatrix.clone();
21602179

2180+
@override
2181+
bool shouldUpdateMatrix(ViewConfiguration oldConfiguration) {
2182+
if (oldConfiguration.runtimeType != runtimeType) {
2183+
// New configuration could have different logic, so we don't know
2184+
// whether it will need a new transform. Return a conservative result.
2185+
return true;
2186+
}
2187+
oldConfiguration as TestViewConfiguration;
2188+
// Compare the matrices directly since they are cached.
2189+
return oldConfiguration._paintMatrix != _paintMatrix;
2190+
}
2191+
21612192
final Size _physicalSize;
21622193

21632194
@override

packages/flutter_test/test/widget_tester_test.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ void main() {
5353
group('the group with retry flag', () {
5454
testWidgets('the test inside it', (WidgetTester tester) async {
5555
addTearDown(() => retried = true);
56+
if (!retried) {
57+
debugPrint('DISREGARD NEXT FAILURE, IT IS EXPECTED');
58+
}
5659
expect(retried, isTrue);
5760
});
5861
}, retry: 1);
@@ -62,6 +65,9 @@ void main() {
6265
bool retried = false;
6366
testWidgets('the test with retry flag', (WidgetTester tester) async {
6467
addTearDown(() => retried = true);
68+
if (!retried) {
69+
debugPrint('DISREGARD NEXT FAILURE, IT IS EXPECTED');
70+
}
6571
expect(retried, isTrue);
6672
}, retry: 1);
6773
});
@@ -557,6 +563,7 @@ void main() {
557563
};
558564

559565
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
566+
debugPrint('DISREGARD NEXT PENDING TIMER LIST, IT IS EXPECTED');
560567
await binding.runTest(() async {
561568
final Timer timer = Timer(const Duration(seconds: 1), () {});
562569
expect(timer.isActive, true);

0 commit comments

Comments
 (0)