Skip to content

Commit

Permalink
Allow the SceneBuilder, PictureRecord, and Canvas constructor calls f…
Browse files Browse the repository at this point in the history
…rom the rendering layer to be hooked (#147271)

This also includes some minor cleanup of documentation, asserts, and tests.
  • Loading branch information
Hixie committed Apr 25, 2024
1 parent c22ed98 commit 9751d4d
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 25 deletions.
27 changes: 26 additions & 1 deletion packages/flutter/lib/src/rendering/binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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].
Expand Down
10 changes: 6 additions & 4 deletions packages/flutter/lib/src/rendering/layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand Down Expand Up @@ -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]) {
Expand Down
5 changes: 3 additions & 2 deletions packages/flutter/lib/src/rendering/object.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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!);
}

Expand Down
62 changes: 53 additions & 9 deletions packages/flutter/lib/src/rendering/view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
///
Expand Down Expand Up @@ -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) {
Expand All @@ -149,17 +175,19 @@ 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);
markNeedsLayout();
}

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

@override
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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();
Expand Down
78 changes: 78 additions & 0 deletions packages/flutter/test/rendering/painting_mocks_test.dart
Original file line number Diff line number Diff line change
@@ -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);
}
}
49 changes: 40 additions & 9 deletions packages/flutter_test/lib/src/binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions packages/flutter_test/test/widget_tester_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
});
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 9751d4d

Please sign in to comment.