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.
This commit is contained in:
Ian Hickson 2024-04-24 17:19:24 -07:00 committed by GitHub
parent c22ed980d0
commit 9751d4d002
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 213 additions and 25 deletions

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui show SemanticsUpdate;
import 'dart:ui' as ui show PictureRecorder, SceneBuilder, SemanticsUpdate;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
@ -350,6 +350,31 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
return ViewConfiguration.fromView(renderView.flutterView);
}
/// Create a [SceneBuilder].
///
/// This hook enables test bindings to instrument the rendering layer.
///
/// This is used by the [RenderView] to create the [SceneBuilder] that is
/// passed to the [Layer] system to render the scene.
ui.SceneBuilder createSceneBuilder() => ui.SceneBuilder();
/// Create a [PictureRecorder].
///
/// This hook enables test bindings to instrument the rendering layer.
///
/// This is used by the [PaintingContext] to create the [PictureRecorder]s
/// used when painting [RenderObject]s into [Picture]s passed to
/// [PictureLayer]s.
ui.PictureRecorder createPictureRecorder() => ui.PictureRecorder();
/// Create a [Canvas] from a [PictureRecorder].
///
/// This hook enables test bindings to instrument the rendering layer.
///
/// This is used by the [PaintingContext] after creating a [PictureRecorder]
/// using [createPictureRecorder].
Canvas createCanvas(ui.PictureRecorder recorder) => Canvas(recorder);
/// Called when the system metrics change.
///
/// See [dart:ui.PlatformDispatcher.onMetricsChanged].

View File

@ -85,10 +85,10 @@ const String _flutterRenderingLibrary = 'package:flutter/rendering.dart';
/// different parents. The scene must be explicitly recomposited after such
/// changes are made; the layer tree does not maintain its own dirty state.
///
/// To composite the tree, create a [SceneBuilder] object, pass it to the
/// root [Layer] object's [addToScene] method, and then call
/// [SceneBuilder.build] to obtain a [Scene]. A [Scene] can then be painted
/// using [dart:ui.FlutterView.render].
/// To composite the tree, create a [SceneBuilder] object using
/// [RendererBinding.createSceneBuilder], pass it to the root [Layer] object's
/// [addToScene] method, and then call [SceneBuilder.build] to obtain a [Scene].
/// A [Scene] can then be painted using [dart:ui.FlutterView.render].
///
/// ## Memory
///
@ -765,6 +765,8 @@ abstract class Layer with DiagnosticableTreeMixin {
/// layer in [RenderObject.paint], it should dispose of the handle to the
/// old layer. It should also dispose of any layer handles it holds in
/// [RenderObject.dispose].
///
/// To dispose of a layer handle, set its [layer] property to null.
class LayerHandle<T extends Layer> {
/// Create a new layer handle, optionally referencing a [Layer].
LayerHandle([this._layer]) {

View File

@ -11,6 +11,7 @@ import 'package:flutter/painting.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/semantics.dart';
import 'binding.dart';
import 'debug.dart';
import 'layer.dart';
@ -331,8 +332,8 @@ class PaintingContext extends ClipContext {
void _startRecording() {
assert(!_isRecording);
_currentLayer = PictureLayer(estimatedBounds);
_recorder = ui.PictureRecorder();
_canvas = Canvas(_recorder!);
_recorder = RendererBinding.instance.createPictureRecorder();
_canvas = RendererBinding.instance.createCanvas(_recorder!);
_containerLayer.append(_currentLayer!);
}

View File

@ -66,6 +66,19 @@ class ViewConfiguration {
return Matrix4.diagonal3Values(devicePixelRatio, devicePixelRatio, 1.0);
}
/// Returns whether [toMatrix] would return a different value for this
/// configuration than it would for the given `oldConfiguration`.
bool shouldUpdateMatrix(ViewConfiguration oldConfiguration) {
if (oldConfiguration.runtimeType != runtimeType) {
// New configuration could have different logic, so we don't know
// whether it will need a new transform. Return a conservative result.
return true;
}
// For this class, the only input to toMatrix is the device pixel ratio,
// so we return true if they differ and false otherwise.
return oldConfiguration.devicePixelRatio != devicePixelRatio;
}
/// Transforms the provided [Size] in logical pixels to physical pixels.
///
/// The [FlutterView.render] method accepts only sizes in physical pixels, but
@ -103,6 +116,16 @@ class ViewConfiguration {
/// The view represents the total output surface of the render tree and handles
/// bootstrapping the rendering pipeline. The view has a unique child
/// [RenderBox], which is required to fill the entire output surface.
///
/// This object must be bootstrapped in a specific order:
///
/// 1. First, set the [configuration] (either in the constructor or after
/// construction).
/// 2. Second, [attach] the object to a [PipelineOwner].
/// 3. Third, use [prepareInitialFrame] to bootstrap the layout and paint logic.
///
/// After the bootstrapping is complete, the [compositeFrame] method may be used
/// to obtain the rendered output.
class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> {
/// Creates the root of the render tree.
///
@ -140,6 +163,9 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
/// [TestFlutterView.physicalSize] on the appropriate [TestFlutterView]
/// (typically [WidgetTester.view]) instead of setting a configuration
/// directly on the [RenderView].
///
/// A [configuration] must be set (either directly or by passing one to the
/// constructor) before calling [prepareInitialFrame].
ViewConfiguration get configuration => _configuration!;
ViewConfiguration? _configuration;
set configuration(ViewConfiguration value) {
@ -149,10 +175,10 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
final ViewConfiguration? oldConfiguration = _configuration;
_configuration = value;
if (_rootTransform == null) {
// [prepareInitialFrame] has not been called yet, nothing to do for now.
// [prepareInitialFrame] has not been called yet, nothing more to do for now.
return;
}
if (oldConfiguration?.toMatrix() != configuration.toMatrix()) {
if (oldConfiguration == null || configuration.shouldUpdateMatrix(oldConfiguration)) {
replaceRootLayer(_updateMatricesAndCreateNewRootLayer());
}
assert(_rootTransform != null);
@ -160,6 +186,8 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
}
/// Whether a [configuration] has been set.
///
/// This must be true before calling [prepareInitialFrame].
bool get hasConfiguration => _configuration != null;
@override
@ -202,15 +230,23 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
/// Bootstrap the rendering pipeline by preparing the first frame.
///
/// This should only be called once, and must be called before changing
/// [configuration]. It is typically called immediately after calling the
/// constructor.
/// This should only be called once. It is typically called immediately after
/// setting the [configuration] the first time (whether by passing one to the
/// constructor, or setting it directly). The [configuration] must have been
/// set before calling this method, and the [RenderView] must have been
/// attached to a [PipelineOwner] using [attach].
///
/// This does not actually schedule the first frame. Call
/// [PipelineOwner.requestVisualUpdate] on [owner] to do that.
/// [PipelineOwner.requestVisualUpdate] on the [owner] to do that.
///
/// This should be called before using any methods that rely on the [layer]
/// being initialized, such as [compositeFrame].
///
/// This method calls [scheduleInitialLayout] and [scheduleInitialPaint].
void prepareInitialFrame() {
assert(owner != null);
assert(_rootTransform == null);
assert(owner != null, 'attach the RenderView to a PipelineOwner before calling prepareInitialFrame');
assert(_rootTransform == null, 'prepareInitialFrame must only be called once'); // set by _updateMatricesAndCreateNewRootLayer
assert(hasConfiguration, 'set a configuration before calling prepareInitialFrame');
scheduleInitialLayout();
scheduleInitialPaint(_updateMatricesAndCreateNewRootLayer());
assert(_rootTransform != null);
@ -219,6 +255,7 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
Matrix4? _rootTransform;
TransformLayer _updateMatricesAndCreateNewRootLayer() {
assert(hasConfiguration);
_rootTransform = configuration.toMatrix();
final TransformLayer rootLayer = TransformLayer(transform: _rootTransform);
rootLayer.attach(this);
@ -295,12 +332,19 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
/// Uploads the composited layer tree to the engine.
///
/// Actually causes the output of the rendering pipeline to appear on screen.
///
/// Before calling this method, the [owner] must be set by calling [attach],
/// the [configuration] must be set to a non-null value, and the
/// [prepareInitialFrame] method must have been called.
void compositeFrame() {
if (!kReleaseMode) {
FlutterTimeline.startSync('COMPOSITING');
}
try {
final ui.SceneBuilder builder = ui.SceneBuilder();
assert(hasConfiguration, 'set the RenderView configuration before calling compositeFrame');
assert(_rootTransform != null, 'call prepareInitialFrame before calling compositeFrame');
assert(layer != null, 'call prepareInitialFrame before calling compositeFrame');
final ui.SceneBuilder builder = RendererBinding.instance.createSceneBuilder();
final ui.Scene scene = layer!.buildScene(builder);
if (automaticSystemUiAdjustment) {
_updateSystemChrome();

View File

@ -0,0 +1,78 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
final List<String> log = <String>[];
void main() {
final PaintingMocksTestRenderingFlutterBinding binding = PaintingMocksTestRenderingFlutterBinding.ensureInitialized();
test('createSceneBuilder et al', () async {
final RenderView root = RenderView(
view: binding.platformDispatcher.views.single,
configuration: const ViewConfiguration(),
);
root.attach(PipelineOwner());
root.prepareInitialFrame();
expect(log, isEmpty);
root.compositeFrame();
expect(log, <String>['createSceneBuilder']);
log.clear();
final PaintingContext context = PaintingContext(ContainerLayer(), Rect.zero);
expect(log, isEmpty);
context.canvas;
expect(log, <String>['createPictureRecorder', 'createCanvas']);
log.clear();
context.addLayer(ContainerLayer());
expect(log, isEmpty);
context.canvas;
expect(log, <String>['createPictureRecorder', 'createCanvas']);
log.clear();
});
}
class PaintingMocksTestRenderingFlutterBinding extends BindingBase with SchedulerBinding, ServicesBinding, GestureBinding, PaintingBinding, SemanticsBinding, RendererBinding {
@override
void initInstances() {
super.initInstances();
_instance = this;
}
static PaintingMocksTestRenderingFlutterBinding get instance => BindingBase.checkInstance(_instance);
static PaintingMocksTestRenderingFlutterBinding? _instance;
static PaintingMocksTestRenderingFlutterBinding ensureInitialized() {
if (PaintingMocksTestRenderingFlutterBinding._instance == null) {
PaintingMocksTestRenderingFlutterBinding();
}
return PaintingMocksTestRenderingFlutterBinding.instance;
}
@override
ui.SceneBuilder createSceneBuilder() {
log.add('createSceneBuilder');
return super.createSceneBuilder();
}
@override
ui.PictureRecorder createPictureRecorder() {
log.add('createPictureRecorder');
return super.createPictureRecorder();
}
@override
Canvas createCanvas(ui.PictureRecorder recorder) {
log.add('createCanvas');
return super.createCanvas(recorder);
}
}

View File

@ -2098,7 +2098,11 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
///
/// The resulting ViewConfiguration maps the given size onto the actual display
/// using the [BoxFit.contain] algorithm.
class TestViewConfiguration extends ViewConfiguration {
///
/// If the underlying [FlutterView] changes, a new [TestViewConfiguration] should
/// be created. See [RendererBinding.handleMetricsChanged] and
/// [RendererBinding.createViewConfigurationFor].
class TestViewConfiguration implements ViewConfiguration {
/// Deprecated. Will be removed in a future version of Flutter.
///
/// This property has been deprecated to prepare for Flutter's upcoming
@ -2120,14 +2124,29 @@ class TestViewConfiguration extends ViewConfiguration {
/// Creates a [TestViewConfiguration] with the given size and view.
///
/// The [size] defaults to 800x600.
TestViewConfiguration.fromView({required ui.FlutterView view, Size size = _kDefaultTestViewportSize})
: _paintMatrix = _getMatrix(size, view.devicePixelRatio, view),
_physicalSize = view.physicalSize,
super(
devicePixelRatio: view.devicePixelRatio,
logicalConstraints: BoxConstraints.tight(size),
physicalConstraints: BoxConstraints.tight(size) * view.devicePixelRatio,
);
///
/// The settings of the given [FlutterView] are captured when the constructor
/// is called, and subsequent changes are ignored. A new
/// [TestViewConfiguration] should be created if the underlying [FlutterView]
/// changes. See [RendererBinding.handleMetricsChanged] and
/// [RendererBinding.createViewConfigurationFor].
TestViewConfiguration.fromView({
required ui.FlutterView view,
Size size = _kDefaultTestViewportSize,
}) : devicePixelRatio = view.devicePixelRatio,
logicalConstraints = BoxConstraints.tight(size),
physicalConstraints = BoxConstraints.tight(size) * view.devicePixelRatio,
_paintMatrix = _getMatrix(size, view.devicePixelRatio, view),
_physicalSize = view.physicalSize;
@override
final double devicePixelRatio;
@override
final BoxConstraints logicalConstraints;
@override
final BoxConstraints physicalConstraints;
static Matrix4 _getMatrix(Size size, double devicePixelRatio, ui.FlutterView window) {
final double inverseRatio = devicePixelRatio / window.devicePixelRatio;
@ -2158,6 +2177,18 @@ class TestViewConfiguration extends ViewConfiguration {
@override
Matrix4 toMatrix() => _paintMatrix.clone();
@override
bool shouldUpdateMatrix(ViewConfiguration oldConfiguration) {
if (oldConfiguration.runtimeType != runtimeType) {
// New configuration could have different logic, so we don't know
// whether it will need a new transform. Return a conservative result.
return true;
}
oldConfiguration as TestViewConfiguration;
// Compare the matrices directly since they are cached.
return oldConfiguration._paintMatrix != _paintMatrix;
}
final Size _physicalSize;
@override

View File

@ -53,6 +53,9 @@ void main() {
group('the group with retry flag', () {
testWidgets('the test inside it', (WidgetTester tester) async {
addTearDown(() => retried = true);
if (!retried) {
debugPrint('DISREGARD NEXT FAILURE, IT IS EXPECTED');
}
expect(retried, isTrue);
});
}, retry: 1);
@ -62,6 +65,9 @@ void main() {
bool retried = false;
testWidgets('the test with retry flag', (WidgetTester tester) async {
addTearDown(() => retried = true);
if (!retried) {
debugPrint('DISREGARD NEXT FAILURE, IT IS EXPECTED');
}
expect(retried, isTrue);
}, retry: 1);
});
@ -557,6 +563,7 @@ void main() {
};
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
debugPrint('DISREGARD NEXT PENDING TIMER LIST, IT IS EXPECTED');
await binding.runTest(() async {
final Timer timer = Timer(const Duration(seconds: 1), () {});
expect(timer.isActive, true);