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:
parent
c22ed980d0
commit
9751d4d002
@ -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].
|
||||
|
@ -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]) {
|
||||
|
@ -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!);
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
78
packages/flutter/test/rendering/painting_mocks_test.dart
Normal file
78
packages/flutter/test/rendering/painting_mocks_test.dart
Normal 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);
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user