Stand-alone widget tree with multiple render trees to enable multi-view rendering (#125003)

This change enables Flutter to generate multiple Scenes to be rendered into separate FlutterViews from a single widget tree. Each Scene is described by a separate render tree, which are all associated with the single widget tree.

This PR implements the framework-side mechanisms to describe the content to be rendered into multiple views. Separate engine-side changes are necessary to provide these views to the framework and to draw the framework-generated Scene into them.

## Summary of changes

The details of this change are described in [flutter.dev/go/multiple-views](https://flutter.dev/go/multiple-views). Below is a high-level summary organized by layers.

### Rendering layer changes

* The `RendererBinding` no longer owns a single `renderView`. In fact, it doesn't OWN any `RenderView`s at all anymore. Instead, it offers an API (`addRenderView`/`removeRenderView`) to add and remove `RenderView`s that then will be MANAGED by the binding. The `RenderView` itself is now owned by a higher-level abstraction (e.g. the `RawView` Element of the widgets layer, see below), who is also in charge of adding it to the binding. When added, the binding will interact with the `RenderView` to produce a frame (e.g. by calling `compositeFrame` on it) and to perform hit tests for incoming pointer events. Multiple `RenderView`s can be added to the binding (typically one per `FlutterView`) to produce multiple Scenes.
* Instead of owning a single `pipelineOwner`, the `RendererBinding` now owns the root of the `PipelineOwner` tree (exposed as `rootPipelineOwner` on the binding). Each `PipelineOwner` in that tree (except for the root) typically manages its own render tree typically rooted in one of the `RenderView`s mentioned in the previous bullet. During frame production, the binding will instruct each `PipelineOwner` of that tree to flush layout, paint, semantics etc. A higher-level abstraction (e.g. the widgets layer, see below) is in charge of adding `PipelineOwner`s to this tree.
* Backwards compatibility: The old `renderView` and `pipelineOwner` properties of the `RendererBinding` are retained, but marked as deprecated. Care has been taken to keep their original behavior for the deprecation period, i.e. if you just call `runApp`, the render tree bootstrapped by this call is rooted in the deprecated `RendererBinding.renderView` and managed by the deprecated `RendererBinding.pipelineOwner`.

### Widgets layer changes

* The `WidgetsBinding` no longer attaches the widget tree to an existing render tree. Instead, it bootstraps a stand-alone widget tree that is not backed by a render tree. For this, `RenderObjectToWidgetAdapter` has been replaced by `RootWidget`.
* Multiple render trees can be bootstrapped and attached to the widget tree with the help of the `View` widget, which internally is backed by a `RawView` widget. Configured with a `FlutterView` to render into, the `RawView` creates a new `PipelineOwner` and a new `RenderView` for the new render tree. It adds the new `RenderView` to the `RendererBinding` and its `PipelineOwner` to the pipeline owner tree.
* The `View` widget can only appear in certain well-defined locations in the widget tree since it bootstraps a new render tree and does not insert a `RenderObject` into an ancestor. However, almost all Elements expect that their children insert `RenderObject`s, otherwise they will not function properly. To produce a good error message when the `View` widget is used in an illegal location, the `debugMustInsertRenderObjectIntoSlot` method has been added to Element, where a child can ask whether a given slot must insert a RenderObject into its ancestor or not. In practice, the `View` widget can be used as a child of the `RootWidget`, inside the `view` slot of the `ViewAnchor` (see below) and inside a `ViewCollection` (see below). In those locations, the `View` widget may be wrapped in other non-RenderObjectWidgets (e.g. InheritedWidgets).
* The new `ViewAnchor` can be used to create a side-view inside a parent `View`. The `child` of the `ViewAnchor` widget renders into the parent `View` as usual, but the `view` slot can take on another `View` widget, which has access to all inherited widgets above the `ViewAnchor`. Metaphorically speaking, the view is anchored to the location of the `ViewAnchor` in the widget tree.
* The new `ViewCollection` widget allows for multiple sibling views as it takes a list of `View`s as children. It can be used in all the places that accept a `View` widget.

## Google3

As of July 5, 2023 this change passed a TAP global presubmit (TGP) in google3: tap/OCL:544707016:BASE:545809771:1688597935864:e43dd651

## Note to reviewers

This change is big (sorry). I suggest focusing the initial review on the changes inside of `packages/flutter` first. The majority of the changes describe above are implemented in (listed in suggested review order):

* `rendering/binding.dart`
* `widgets/binding.dart`
* `widgets/view.dart`
* `widgets/framework.dart`

All other changes included in the PR are basically the fallout of what's implemented in those files. Also note that a lot of the lines added in this PR are documentation and tests.

I am also very happy to walk reviewers through the code in person or via video call, if that is helpful.

I appreciate any feedback.

## Feedback to address before submitting ("TODO")
This commit is contained in:
Michael Goderbauer 2023-07-17 09:14:08 -07:00 committed by GitHub
parent 7a42ed7ef6
commit 6f09064e78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 4596 additions and 535 deletions

View File

@ -39,7 +39,7 @@ Future<void> main() async {
size: const Size(355.0, 635.0), size: const Size(355.0, 635.0),
view: tester.view, view: tester.view,
); );
final RenderView renderView = WidgetsBinding.instance.renderView; final RenderView renderView = WidgetsBinding.instance.renderViews.single;
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.benchmark; binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.benchmark;
watch.start(); watch.start();

View File

@ -1361,7 +1361,7 @@ Future<void> _runWebTreeshakeTest() async {
final String javaScript = mainDartJs.readAsStringSync(); final String javaScript = mainDartJs.readAsStringSync();
// Check that we're not looking at minified JS. Otherwise this test would result in false positive. // Check that we're not looking at minified JS. Otherwise this test would result in false positive.
expect(javaScript.contains('RenderObjectToWidgetElement'), true); expect(javaScript.contains('RootElement'), true);
const String word = 'debugFillProperties'; const String word = 'debugFillProperties';
int count = 0; int count = 0;

View File

@ -79,8 +79,8 @@ Future<void> smokeDemo(WidgetTester tester, GalleryDemo demo) async {
// Verify that the dumps are pretty. // Verify that the dumps are pretty.
final String routeName = demo.routeName; final String routeName = demo.routeName;
verifyToStringOutput('debugDumpApp', routeName, WidgetsBinding.instance.rootElement!.toStringDeep()); verifyToStringOutput('debugDumpApp', routeName, WidgetsBinding.instance.rootElement!.toStringDeep());
verifyToStringOutput('debugDumpRenderTree', routeName, RendererBinding.instance.renderView.toStringDeep()); verifyToStringOutput('debugDumpRenderTree', routeName, RendererBinding.instance.renderViews.single.toStringDeep());
verifyToStringOutput('debugDumpLayerTree', routeName, RendererBinding.instance.renderView.debugLayer?.toStringDeep() ?? ''); verifyToStringOutput('debugDumpLayerTree', routeName, RendererBinding.instance.renderViews.single.debugLayer?.toStringDeep() ?? '');
verifyToStringOutput('debugDumpFocusTree', routeName, WidgetsBinding.instance.focusManager.toStringDeep()); verifyToStringOutput('debugDumpFocusTree', routeName, WidgetsBinding.instance.focusManager.toStringDeep());
// Scroll the demo around a bit more. // Scroll the demo around a bit more.

View File

@ -6,6 +6,7 @@
// system. Most of the guts of this examples are in src/sector_layout.dart. // system. Most of the guts of this examples are in src/sector_layout.dart.
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'src/binding.dart';
import 'src/sector_layout.dart'; import 'src/sector_layout.dart';
RenderBox buildSectorExample() { RenderBox buildSectorExample() {
@ -21,5 +22,5 @@ RenderBox buildSectorExample() {
} }
void main() { void main() {
RenderingFlutterBinding(root: buildSectorExample()).scheduleFrame(); ViewRenderingFlutterBinding(root: buildSectorExample()).scheduleFrame();
} }

View File

@ -7,6 +7,7 @@
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'src/binding.dart';
import 'src/solid_color_box.dart'; import 'src/solid_color_box.dart';
void main() { void main() {
@ -86,5 +87,5 @@ void main() {
child: RenderPadding(child: table, padding: const EdgeInsets.symmetric(vertical: 50.0)), child: RenderPadding(child: table, padding: const EdgeInsets.symmetric(vertical: 50.0)),
); );
RenderingFlutterBinding(root: root).scheduleFrame(); ViewRenderingFlutterBinding(root: root).scheduleFrame();
} }

View File

@ -7,9 +7,11 @@
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'src/binding.dart';
void main() { void main() {
// We use RenderingFlutterBinding to attach the render tree to the window. // We use ViewRenderingFlutterBinding to attach the render tree to the window.
RenderingFlutterBinding( ViewRenderingFlutterBinding(
// The root of our render tree is a RenderPositionedBox, which centers its // The root of our render tree is a RenderPositionedBox, which centers its
// child both vertically and horizontally. // child both vertically and horizontally.
root: RenderPositionedBox( root: RenderPositionedBox(

View File

@ -11,6 +11,8 @@ import 'package:flutter/animation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'src/binding.dart';
class NonStopVSync implements TickerProvider { class NonStopVSync implements TickerProvider {
const NonStopVSync(); const NonStopVSync();
@override @override
@ -42,7 +44,7 @@ void main() {
child: spin, child: spin,
); );
// and attach it to the window. // and attach it to the window.
RenderingFlutterBinding(root: root); ViewRenderingFlutterBinding(root: root);
// To make the square spin, we use an animation that repeats every 1800 // To make the square spin, we use an animation that repeats every 1800
// milliseconds. // milliseconds.

View File

@ -0,0 +1,69 @@
// 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';
import 'package:flutter/rendering.dart';
/// An extension of [RenderingFlutterBinding] that owns and manages a
/// [renderView].
///
/// Unlike [RenderingFlutterBinding], this binding also creates and owns a
/// [renderView] to simplify bootstrapping for apps that have a dedicated main
/// view.
class ViewRenderingFlutterBinding extends RenderingFlutterBinding {
/// Creates a binding for the rendering layer.
///
/// The `root` render box is attached directly to the [renderView] and is
/// given constraints that require it to fill the window. The [renderView]
/// itself is attached to the [rootPipelineOwner].
///
/// This binding does not automatically schedule any frames. Callers are
/// responsible for deciding when to first call [scheduleFrame].
ViewRenderingFlutterBinding({ RenderBox? root }) : _root = root;
@override
void initInstances() {
super.initInstances();
// TODO(goderbauer): Create window if embedder doesn't provide an implicit view.
assert(PlatformDispatcher.instance.implicitView != null);
_renderView = initRenderView(PlatformDispatcher.instance.implicitView!);
_renderView.child = _root;
_root = null;
}
RenderBox? _root;
@override
RenderView get renderView => _renderView;
late RenderView _renderView;
/// Creates a [RenderView] object to be the root of the
/// [RenderObject] rendering tree, and initializes it so that it
/// will be rendered when the next frame is requested.
///
/// Called automatically when the binding is created.
RenderView initRenderView(FlutterView view) {
final RenderView renderView = RenderView(view: view);
rootPipelineOwner.rootNode = renderView;
addRenderView(renderView);
renderView.prepareInitialFrame();
return renderView;
}
@override
PipelineOwner createRootPipelineOwner() {
return PipelineOwner(
onSemanticsOwnerCreated: () {
renderView.scheduleInitialSemantics();
},
onSemanticsUpdate: (SemanticsUpdate update) {
renderView.updateSemantics(update);
},
onSemanticsOwnerDisposed: () {
renderView.clearSemantics();
},
);
}
}

View File

@ -8,6 +8,8 @@
import 'package:flutter/material.dart'; // Imported just for its color palette. import 'package:flutter/material.dart'; // Imported just for its color palette.
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'src/binding.dart';
// Material design colors. :p // Material design colors. :p
List<Color> _kColors = <Color>[ List<Color> _kColors = <Color>[
Colors.teal, Colors.teal,
@ -133,5 +135,5 @@ void main() {
..left = 20.0; ..left = 20.0;
// Finally, we attach the render tree we've built to the screen. // Finally, we attach the render tree we've built to the screen.
RenderingFlutterBinding(root: stack).scheduleFrame(); ViewRenderingFlutterBinding(root: stack).scheduleFrame();
} }

View File

@ -52,10 +52,10 @@ void attachWidgetTreeToRenderTree(RenderProxyBox container) {
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[ children: <Widget>[
ElevatedButton( ElevatedButton(
child: Row( child: const Row(
children: <Widget>[ children: <Widget>[
Image.network('https://flutter.dev/images/favicon.png'), FlutterLogo(),
const Text('PRESS ME'), Text('PRESS ME'),
], ],
), ),
onPressed: () { onPressed: () {
@ -102,6 +102,16 @@ void main() {
transformBox = RenderTransform(child: flexRoot, transform: Matrix4.identity(), alignment: Alignment.center); transformBox = RenderTransform(child: flexRoot, transform: Matrix4.identity(), alignment: Alignment.center);
final RenderPadding root = RenderPadding(padding: const EdgeInsets.all(80.0), child: transformBox); final RenderPadding root = RenderPadding(padding: const EdgeInsets.all(80.0), child: transformBox);
binding.renderView.child = root; // TODO(goderbauer): Create a window if embedder doesn't provide an implicit view to draw into.
assert(binding.platformDispatcher.implicitView != null);
final RenderView view = RenderView(
view: binding.platformDispatcher.implicitView!,
child: root,
);
final PipelineOwner pipelineOwner = PipelineOwner()..rootNode = view;
binding.rootPipelineOwner.adoptChild(pipelineOwner);
binding.addRenderView(view);
view.prepareInitialFrame();
binding.addPersistentFrameCallback(rotate); binding.addPersistentFrameCallback(rotate);
} }

View File

@ -10,7 +10,6 @@ import 'package:flutter/scheduler.dart';
import 'package:flutter/semantics.dart'; import 'package:flutter/semantics.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'box.dart';
import 'debug.dart'; import 'debug.dart';
import 'mouse_tracker.dart'; import 'mouse_tracker.dart';
import 'object.dart'; import 'object.dart';
@ -22,28 +21,34 @@ export 'package:flutter/gestures.dart' show HitTestResult;
// Examples can assume: // Examples can assume:
// late BuildContext context; // late BuildContext context;
/// The glue between the render tree and the Flutter engine. /// The glue between the render trees and the Flutter engine.
///
/// The [RendererBinding] manages multiple independent render trees. Each render
/// tree is rooted in a [RenderView] that must be added to the binding via
/// [addRenderView] to be considered during frame production, hit testing, etc.
/// Furthermore, the render tree must be managed by a [PipelineOwner] that is
/// part of the pipeline owner tree rooted at [rootPipelineOwner].
///
/// Adding [PipelineOwner]s and [RenderView]s to this binding in the way
/// described above is left as a responsibility for a higher level abstraction.
/// The widgets library, for example, introduces the [View] widget, which
/// registers its [RenderView] and [PipelineOwner] with this binding.
mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable { mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable {
@override @override
void initInstances() { void initInstances() {
super.initInstances(); super.initInstances();
_instance = this; _instance = this;
_pipelineOwner = PipelineOwner( _rootPipelineOwner = createRootPipelineOwner();
onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,
onSemanticsUpdate: _handleSemanticsUpdate,
onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,
);
platformDispatcher platformDispatcher
..onMetricsChanged = handleMetricsChanged ..onMetricsChanged = handleMetricsChanged
..onTextScaleFactorChanged = handleTextScaleFactorChanged ..onTextScaleFactorChanged = handleTextScaleFactorChanged
..onPlatformBrightnessChanged = handlePlatformBrightnessChanged; ..onPlatformBrightnessChanged = handlePlatformBrightnessChanged;
initRenderView();
addPersistentFrameCallback(_handlePersistentFrameCallback); addPersistentFrameCallback(_handlePersistentFrameCallback);
initMouseTracker(); initMouseTracker();
if (kIsWeb) { if (kIsWeb) {
addPostFrameCallback(_handleWebFirstFrame); addPostFrameCallback(_handleWebFirstFrame);
} }
_pipelineOwner.attach(_manifold); rootPipelineOwner.attach(_manifold);
} }
/// The current [RendererBinding], if one has been created. /// The current [RendererBinding], if one has been created.
@ -108,9 +113,8 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
registerServiceExtension( registerServiceExtension(
name: RenderingServiceExtensions.debugDumpLayerTree.name, name: RenderingServiceExtensions.debugDumpLayerTree.name,
callback: (Map<String, String> parameters) async { callback: (Map<String, String> parameters) async {
final String data = RendererBinding.instance.renderView.debugLayer?.toStringDeep() ?? 'Layer tree unavailable.';
return <String, Object>{ return <String, Object>{
'data': data, 'data': _debugCollectLayerTrees(),
}; };
}, },
); );
@ -155,9 +159,8 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
registerServiceExtension( registerServiceExtension(
name: RenderingServiceExtensions.debugDumpRenderTree.name, name: RenderingServiceExtensions.debugDumpRenderTree.name,
callback: (Map<String, String> parameters) async { callback: (Map<String, String> parameters) async {
final String data = RendererBinding.instance.renderView.toStringDeep();
return <String, Object>{ return <String, Object>{
'data': data, 'data': _debugCollectRenderTrees(),
}; };
}, },
); );
@ -165,7 +168,7 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
name: RenderingServiceExtensions.debugDumpSemanticsTreeInTraversalOrder.name, name: RenderingServiceExtensions.debugDumpSemanticsTreeInTraversalOrder.name,
callback: (Map<String, String> parameters) async { callback: (Map<String, String> parameters) async {
return <String, Object>{ return <String, Object>{
'data': _generateSemanticsTree(DebugSemanticsDumpOrder.traversalOrder), 'data': _debugCollectSemanticsTrees(DebugSemanticsDumpOrder.traversalOrder),
}; };
}, },
); );
@ -173,7 +176,7 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
name: RenderingServiceExtensions.debugDumpSemanticsTreeInInverseHitTestOrder.name, name: RenderingServiceExtensions.debugDumpSemanticsTreeInInverseHitTestOrder.name,
callback: (Map<String, String> parameters) async { callback: (Map<String, String> parameters) async {
return <String, Object>{ return <String, Object>{
'data': _generateSemanticsTree(DebugSemanticsDumpOrder.inverseHitTest), 'data': _debugCollectSemanticsTrees(DebugSemanticsDumpOrder.inverseHitTest),
}; };
}, },
); );
@ -200,38 +203,156 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
late final PipelineManifold _manifold = _BindingPipelineManifold(this); late final PipelineManifold _manifold = _BindingPipelineManifold(this);
/// Creates a [RenderView] object to be the root of the
/// [RenderObject] rendering tree, and initializes it so that it
/// will be rendered when the next frame is requested.
///
/// Called automatically when the binding is created.
void initRenderView() {
assert(!_debugIsRenderViewInitialized);
assert(() {
_debugIsRenderViewInitialized = true;
return true;
}());
renderView = RenderView(configuration: createViewConfiguration(), view: platformDispatcher.implicitView!);
renderView.prepareInitialFrame();
}
bool _debugIsRenderViewInitialized = false;
/// The object that manages state about currently connected mice, for hover /// The object that manages state about currently connected mice, for hover
/// notification. /// notification.
MouseTracker get mouseTracker => _mouseTracker!; MouseTracker get mouseTracker => _mouseTracker!;
MouseTracker? _mouseTracker; MouseTracker? _mouseTracker;
/// The render tree's owner, which maintains dirty state for layout, /// Deprecated. Will be removed in a future version of Flutter.
/// composite, paint, and accessibility semantics. ///
PipelineOwner get pipelineOwner => _pipelineOwner; /// This is typically the owner of the render tree bootstrapped by [runApp]
late PipelineOwner _pipelineOwner; /// and rooted in [renderView]. It maintains dirty state for layout,
/// composite, paint, and accessibility semantics for that tree.
///
/// However, by default, the [pipelineOwner] does not participate in frame
/// production because it is not automatically attached to the
/// [rootPipelineOwner] or any of its descendants. It is also not
/// automatically associated with the [renderView]. This is left as a
/// responsibility for a higher level abstraction. The [WidgetsBinding], for
/// example, wires this up in [WidgetsBinding.wrapWithDefaultView], which is
/// called indirectly from [runApp].
///
/// Apps, that don't use the [WidgetsBinding] or don't call [runApp] (or
/// [WidgetsBinding.wrapWithDefaultView]) must manually add this pipeline owner
/// to the pipeline owner tree rooted at [rootPipelineOwner] and assign a
/// [RenderView] to it if the they want to use this deprecated property.
///
/// Instead of accessing this deprecated property, consider interacting with
/// the root of the [PipelineOwner] tree (exposed in [rootPipelineOwner]) or
/// instead of accessing the [SemanticsOwner] of any [PipelineOwner] consider
/// interacting with the [SemanticsBinding] (exposed via
/// [SemanticsBinding.instance]) directly.
@Deprecated(
'Interact with the pipelineOwner tree rooted at RendererBinding.rootPipelineOwner instead. '
'Or instead of accessing the SemanticsOwner of any PipelineOwner interact with the SemanticsBinding directly. '
'This feature was deprecated after v3.10.0-12.0.pre.'
)
late final PipelineOwner pipelineOwner = PipelineOwner(
onSemanticsOwnerCreated: () {
(pipelineOwner.rootNode as RenderView?)?.scheduleInitialSemantics();
},
onSemanticsUpdate: (ui.SemanticsUpdate update) {
(pipelineOwner.rootNode as RenderView?)?.updateSemantics(update);
},
onSemanticsOwnerDisposed: () {
(pipelineOwner.rootNode as RenderView?)?.clearSemantics();
}
);
/// The render tree that's attached to the output surface. /// Deprecated. Will be removed in a future version of Flutter.
RenderView get renderView => _pipelineOwner.rootNode! as RenderView; ///
/// Sets the given [RenderView] object (which must not be null), and its tree, to /// This is typically the root of the render tree bootstrapped by [runApp].
/// be the new render tree to display. The previous tree, if any, is detached. ///
set renderView(RenderView value) { /// However, by default this render view is not associated with any
_pipelineOwner.rootNode = value; /// [PipelineOwner] and therefore isn't considered during frame production.
/// It is also not registered with this binding via [addRenderView].
/// Wiring this up is left as a responsibility for a higher level. The
/// [WidgetsBinding], for example, sets this up in
/// [WidgetsBinding.wrapWithDefaultView], which is called indirectly from
/// [runApp].
///
/// Apps that don't use the [WidgetsBinding] or don't call [runApp] (or
/// [WidgetsBinding.wrapWithDefaultView]) must manually assign a
/// [PipelineOwner] to this [RenderView], make sure the pipeline owner is part
/// of the pipeline owner tree rooted at [rootPipelineOwner], and call
/// [addRenderView] if they want to use this deprecated property.
///
/// Instead of interacting with this deprecated property, consider using
/// [renderViews] instead, which contains all [RenderView]s managed by the
/// binding.
@Deprecated(
'Consider using RendererBinding.renderViews instead as the binding may manage multiple RenderViews. '
'This feature was deprecated after v3.10.0-12.0.pre.'
)
// TODO(goderbauer): When this deprecated property is removed also delete the _ReusableRenderView class.
late final RenderView renderView = _ReusableRenderView(
view: platformDispatcher.implicitView!,
);
/// Creates the [PipelineOwner] that serves as the root of the pipeline owner
/// tree ([rootPipelineOwner]).
///
/// {@template flutter.rendering.createRootPipelineOwner}
/// By default, the root pipeline owner is not setup to manage a render tree
/// and its [PipelineOwner.rootNode] must not be assigned. If necessary,
/// [createRootPipelineOwner] may be overridden to create a root pipeline
/// owner configured to manage its own render tree.
///
/// In typical use, child pipeline owners are added to the root pipeline owner
/// (via [PipelineOwner.adoptChild]). Those children typically do each manage
/// their own [RenderView] and produce distinct render trees which render
/// their content into the [FlutterView] associated with that [RenderView].
/// {@endtemplate}
PipelineOwner createRootPipelineOwner() {
return _DefaultRootPipelineOwner();
}
/// The [PipelineOwner] that is the root of the PipelineOwner tree.
///
/// {@macro flutter.rendering.createRootPipelineOwner}
PipelineOwner get rootPipelineOwner => _rootPipelineOwner;
late PipelineOwner _rootPipelineOwner;
/// The [RenderView]s managed by this binding.
///
/// A [RenderView] is added by [addRenderView] and removed by [removeRenderView].
Iterable<RenderView> get renderViews => _viewIdToRenderView.values;
final Map<Object, RenderView> _viewIdToRenderView = <Object, RenderView>{};
/// Adds a [RenderView] to this binding.
///
/// The binding will interact with the [RenderView] in the following ways:
///
/// * setting and updating [RenderView.configuration],
/// * calling [RenderView.compositeFrame] when it is time to produce a new
/// frame, and
/// * forwarding relevant pointer events to the [RenderView] for hit testing.
///
/// To remove a [RenderView] from the binding, call [removeRenderView].
void addRenderView(RenderView view) {
final Object viewId = view.flutterView.viewId;
assert(!_viewIdToRenderView.containsValue(view));
assert(!_viewIdToRenderView.containsKey(viewId));
_viewIdToRenderView[viewId] = view;
view.configuration = createViewConfigurationFor(view);
}
/// Removes a [RenderView] previously added with [addRenderView] from the
/// binding.
void removeRenderView(RenderView view) {
final Object viewId = view.flutterView.viewId;
assert(_viewIdToRenderView[viewId] == view);
_viewIdToRenderView.remove(viewId);
}
/// Returns a [ViewConfiguration] configured for the provided [RenderView]
/// based on the current environment.
///
/// This is called during [addRenderView] and also in response to changes to
/// the system metrics to update all [renderViews] added to the binding.
///
/// Bindings can override this method to change what size or device pixel
/// ratio the [RenderView] will use. For example, the testing framework uses
/// this to force the display into 800x600 when a test is run on the device
/// using `flutter run`.
@protected
ViewConfiguration createViewConfigurationFor(RenderView renderView) {
final FlutterView view = renderView.flutterView;
final double devicePixelRatio = view.devicePixelRatio;
return ViewConfiguration(
size: view.physicalSize / devicePixelRatio,
devicePixelRatio: devicePixelRatio,
);
} }
/// Called when the system metrics change. /// Called when the system metrics change.
@ -240,8 +361,12 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
@protected @protected
@visibleForTesting @visibleForTesting
void handleMetricsChanged() { void handleMetricsChanged() {
renderView.configuration = createViewConfiguration(); bool forceFrame = false;
if (renderView.child != null) { for (final RenderView view in renderViews) {
forceFrame = forceFrame || view.child != null;
view.configuration = createViewConfigurationFor(view);
}
if (forceFrame) {
scheduleForcedFrame(); scheduleForcedFrame();
} }
} }
@ -288,25 +413,6 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
@protected @protected
void handlePlatformBrightnessChanged() { } void handlePlatformBrightnessChanged() { }
/// Returns a [ViewConfiguration] configured for the [RenderView] based on the
/// current environment.
///
/// This is called during construction and also in response to changes to the
/// system metrics.
///
/// Bindings can override this method to change what size or device pixel
/// ratio the [RenderView] will use. For example, the testing framework uses
/// this to force the display into 800x600 when a test is run on the device
/// using `flutter run`.
ViewConfiguration createViewConfiguration() {
final FlutterView view = platformDispatcher.implicitView!;
final double devicePixelRatio = view.devicePixelRatio;
return ViewConfiguration(
size: view.physicalSize / devicePixelRatio,
devicePixelRatio: devicePixelRatio,
);
}
/// Creates a [MouseTracker] which manages state about currently connected /// Creates a [MouseTracker] which manages state about currently connected
/// mice, for hover notification. /// mice, for hover notification.
/// ///
@ -335,19 +441,10 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
@override @override
void performSemanticsAction(SemanticsActionEvent action) { void performSemanticsAction(SemanticsActionEvent action) {
_pipelineOwner.semanticsOwner?.performAction(action.nodeId, action.type, action.arguments); // Due to the asynchronicity in some screen readers (they may not have
} // processed the latest semantics update yet) this code is more forgiving
// and actions for views/nodes that no longer exist are gracefully ignored.
void _handleSemanticsOwnerCreated() { _viewIdToRenderView[action.viewId]?.owner?.semanticsOwner?.performAction(action.nodeId, action.type, action.arguments);
renderView.scheduleInitialSemantics();
}
void _handleSemanticsUpdate(ui.SemanticsUpdate update) {
renderView.updateSemantics(update);
}
void _handleSemanticsOwnerDisposed() {
renderView.clearSemantics();
} }
void _handleWebFirstFrame(Duration _) { void _handleWebFirstFrame(Duration _) {
@ -491,12 +588,14 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
// When editing the above, also update widgets/binding.dart's copy. // When editing the above, also update widgets/binding.dart's copy.
@protected @protected
void drawFrame() { void drawFrame() {
pipelineOwner.flushLayout(); rootPipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits(); rootPipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint(); rootPipelineOwner.flushPaint();
if (sendFramesToEngine) { if (sendFramesToEngine) {
for (final RenderView renderView in renderViews) {
renderView.compositeFrame(); // this sends the bits to the GPU renderView.compositeFrame(); // this sends the bits to the GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS. }
rootPipelineOwner.flushSemantics(); // this sends the semantics to the OS.
_firstFrameSent = true; _firstFrameSent = true;
} }
} }
@ -509,7 +608,9 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
FlutterTimeline.startSync('Preparing Hot Reload (layout)'); FlutterTimeline.startSync('Preparing Hot Reload (layout)');
} }
try { try {
for (final RenderView renderView in renderViews) {
renderView.reassemble(); renderView.reassemble();
}
} finally { } finally {
if (!kReleaseMode) { if (!kReleaseMode) {
FlutterTimeline.finishSync(); FlutterTimeline.finishSync();
@ -520,18 +621,9 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
await endOfFrame; await endOfFrame;
} }
late final int _implicitViewId = platformDispatcher.implicitView!.viewId;
@override @override
void hitTestInView(HitTestResult result, Offset position, int viewId) { void hitTestInView(HitTestResult result, Offset position, int viewId) {
// Currently Flutter only supports one view, the implicit view `renderView`. _viewIdToRenderView[viewId]?.hitTest(result, position: position);
// TODO(dkwingsmt): After Flutter supports multi-view, look up the correct
// render view for the ID.
// https://github.com/flutter/flutter/issues/121573
assert(viewId == _implicitViewId,
'Unexpected view ID $viewId (expecting implicit view ID $_implicitViewId)');
assert(viewId == renderView.flutterView.viewId);
renderView.hitTest(result, position: position);
super.hitTestInView(result, position, viewId); super.hitTestInView(result, position, viewId);
} }
@ -541,40 +633,93 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
child.markNeedsPaint(); child.markNeedsPaint();
child.visitChildren(visitor); child.visitChildren(visitor);
}; };
instance.renderView.visitChildren(visitor); for (final RenderView renderView in renderViews) {
renderView.visitChildren(visitor);
}
return endOfFrame; return endOfFrame;
} }
} }
/// Prints a textual representation of the entire render tree. String _debugCollectRenderTrees() {
if (RendererBinding.instance.renderViews.isEmpty) {
return 'No render tree root was added to the binding.';
}
return <String>[
for (final RenderView renderView in RendererBinding.instance.renderViews)
renderView.toStringDeep(),
].join('\n\n');
}
/// Prints a textual representation of the render trees.
///
/// {@template flutter.rendering.debugDumpRenderTree}
/// It prints the trees associated with every [RenderView] in
/// [RendererBinding.renderView], separated by two blank lines.
/// {@endtemplate}
void debugDumpRenderTree() { void debugDumpRenderTree() {
debugPrint(RendererBinding.instance.renderView.toStringDeep()); debugPrint(_debugCollectRenderTrees());
} }
/// Prints a textual representation of the entire layer tree. String _debugCollectLayerTrees() {
if (RendererBinding.instance.renderViews.isEmpty) {
return 'No render tree root was added to the binding.';
}
return <String>[
for (final RenderView renderView in RendererBinding.instance.renderViews)
renderView.debugLayer?.toStringDeep() ?? 'Layer tree unavailable for $renderView.',
].join('\n\n');
}
/// Prints a textual representation of the layer trees.
///
/// {@macro flutter.rendering.debugDumpRenderTree}
void debugDumpLayerTree() { void debugDumpLayerTree() {
debugPrint(RendererBinding.instance.renderView.debugLayer?.toStringDeep()); debugPrint(_debugCollectLayerTrees());
} }
/// Prints a textual representation of the entire semantics tree. String _debugCollectSemanticsTrees(DebugSemanticsDumpOrder childOrder) {
/// This will only work if there is a semantics client attached. if (RendererBinding.instance.renderViews.isEmpty) {
/// Otherwise, a notice that no semantics are available will be printed. return 'No render tree root was added to the binding.';
}
const String explanation = 'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n'
'Usually, platforms only ask for semantics when assistive technologies (like screen readers) are running.\n'
'To generate semantics, try turning on an assistive technology (like VoiceOver or TalkBack) on your device.';
final List<String> trees = <String>[];
bool printedExplanation = false;
for (final RenderView renderView in RendererBinding.instance.renderViews) {
final String? tree = renderView.debugSemantics?.toStringDeep(childOrder: childOrder);
if (tree != null) {
trees.add(tree);
} else {
String message = 'Semantics not generated for $renderView.';
if (!printedExplanation) {
printedExplanation = true;
message = '$message\n$explanation';
}
trees.add(message);
}
}
return trees.join('\n\n');
}
/// Prints a textual representation of the semantics trees.
///
/// {@macro flutter.rendering.debugDumpRenderTree}
///
/// Semantics trees are only constructed when semantics are enabled (see
/// [SemanticsBinding.semanticsEnabled]). If a semantics tree is not available,
/// a notice about the missing semantics tree is printed instead.
/// ///
/// The order in which the children of a [SemanticsNode] will be printed is /// The order in which the children of a [SemanticsNode] will be printed is
/// controlled by the [childOrder] parameter. /// controlled by the [childOrder] parameter.
void debugDumpSemanticsTree([DebugSemanticsDumpOrder childOrder = DebugSemanticsDumpOrder.traversalOrder]) { void debugDumpSemanticsTree([DebugSemanticsDumpOrder childOrder = DebugSemanticsDumpOrder.traversalOrder]) {
debugPrint(_generateSemanticsTree(childOrder)); debugPrint(_debugCollectSemanticsTrees(childOrder));
} }
String _generateSemanticsTree(DebugSemanticsDumpOrder childOrder) { /// Prints a textual representation of the [PipelineOwner] tree rooted at
final String? tree = RendererBinding.instance.renderView.debugSemantics?.toStringDeep(childOrder: childOrder); /// [RendererBinding.rootPipelineOwner].
if (tree != null) { void debugDumpPipelineOwnerTree() {
return tree; debugPrint(RendererBinding.instance.rootPipelineOwner.toStringDeep());
}
return 'Semantics not generated.\n'
'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n'
'Usually, platforms only ask for semantics when assistive technologies (like screen readers) are running.\n'
'To generate semantics, try turning on an assistive technology (like VoiceOver or TalkBack) on your device.';
} }
/// A concrete binding for applications that use the Rendering framework /// A concrete binding for applications that use the Rendering framework
@ -595,18 +740,17 @@ String _generateSemanticsTree(DebugSemanticsDumpOrder childOrder) {
/// rendering layer directly. If you are writing to a higher-level /// rendering layer directly. If you are writing to a higher-level
/// library, such as the Flutter Widgets library, then you would use /// library, such as the Flutter Widgets library, then you would use
/// that layer's binding (see [WidgetsFlutterBinding]). /// that layer's binding (see [WidgetsFlutterBinding]).
///
/// The [RenderingFlutterBinding] can manage multiple render trees. Each render
/// tree is rooted in a [RenderView] that must be added to the binding via
/// [addRenderView] to be consider during frame production, hit testing, etc.
/// Furthermore, the render tree must be managed by a [PipelineOwner] that is
/// part of the pipeline owner tree rooted at [rootPipelineOwner].
///
/// Adding [PipelineOwner]s and [RenderView]s to this binding in the way
/// described above is left as a responsibility for a higher level abstraction.
/// The binding does not own any [RenderView]s directly.
class RenderingFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, SemanticsBinding, PaintingBinding, RendererBinding { class RenderingFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, SemanticsBinding, PaintingBinding, RendererBinding {
/// Creates a binding for the rendering layer.
///
/// The `root` render box is attached directly to the [renderView] and is
/// given constraints that require it to fill the window.
///
/// This binding does not automatically schedule any frames. Callers are
/// responsible for deciding when to first call [scheduleFrame].
RenderingFlutterBinding({ RenderBox? root }) {
renderView.child = root;
}
/// Returns an instance of the binding that implements /// Returns an instance of the binding that implements
/// [RendererBinding]. If no binding has yet been initialized, the /// [RendererBinding]. If no binding has yet been initialized, the
/// [RenderingFlutterBinding] class is used to create and initialize /// [RenderingFlutterBinding] class is used to create and initialize
@ -645,3 +789,82 @@ class _BindingPipelineManifold extends ChangeNotifier implements PipelineManifol
super.dispose(); super.dispose();
} }
} }
// A [PipelineOwner] that cannot have a root node.
class _DefaultRootPipelineOwner extends PipelineOwner {
_DefaultRootPipelineOwner() : super(onSemanticsUpdate: _onSemanticsUpdate);
@override
set rootNode(RenderObject? _) {
assert(() {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'Cannot set a rootNode on the default root pipeline owner.',
),
ErrorDescription(
'By default, the RendererBinding.rootPipelineOwner is not configured '
'to manage a root node because this pipeline owner does not define a '
'proper onSemanticsUpdate callback to handle semantics for that node.',
),
ErrorHint(
'Typically, the root pipeline owner does not manage a root node. '
'Instead, properly configured child pipeline owners (which do manage '
'root nodes) are added to it. Alternatively, if you do want to set a '
'root node for the root pipeline owner, override '
'RendererBinding.createRootPipelineOwner to create a '
'pipeline owner that is configured to properly handle semantics for '
'the provided root node.'
),
]);
}());
}
static void _onSemanticsUpdate(ui.SemanticsUpdate _) {
// Neve called because we don't have a root node.
assert(false);
}
}
// Prior to multi view support, the [RendererBinding] would own a long-lived
// [RenderView], that was never disposed (see [RendererBinding.renderView]).
// With multi view support, the [RendererBinding] no longer owns a [RenderView]
// and instead higher level abstractions (like the [View] widget) can add/remove
// multiple [RenderView]s to the binding as needed. When the [View] widget is no
// longer needed, it expects to dispose its [RenderView].
//
// This special version of a [RenderView] now exists as a bridge between those
// worlds to continue supporting the [RendererBinding.renderView] property
// through its deprecation period. Per the property's contract, it is supposed
// to be long-lived, but it is also managed by a [View] widget (introduced by
// [WidgetsBinding.wrapWithDefaultView]), that expects to dispose its render
// object at the end of the widget's life time. This special version now
// implements logic to reset the [RenderView] when it is "disposed" so it can be
// reused by another [View] widget.
//
// Once the deprecated [RendererBinding.renderView] property is removed, this
// class is no longer necessary.
class _ReusableRenderView extends RenderView {
_ReusableRenderView({required super.view});
bool _initialFramePrepared = false;
@override
void prepareInitialFrame() {
if (_initialFramePrepared) {
return;
}
super.prepareInitialFrame();
_initialFramePrepared = true;
}
@override
void scheduleInitialSemantics() {
clearSemantics();
super.scheduleInitialSemantics();
}
@override
void dispose() { // ignore: must_call_super
child = null;
}
}

View File

@ -870,7 +870,7 @@ class _LocalSemanticsHandle implements SemanticsHandle {
/// without tying it to a specific binding implementation. All [PipelineOwner]s /// without tying it to a specific binding implementation. All [PipelineOwner]s
/// in a given tree must be attached to the same [PipelineManifold]. This /// in a given tree must be attached to the same [PipelineManifold]. This
/// happens automatically during [adoptChild]. /// happens automatically during [adoptChild].
class PipelineOwner { class PipelineOwner with DiagnosticableTreeMixin {
/// Creates a pipeline owner. /// Creates a pipeline owner.
/// ///
/// Typically created by the binding (e.g., [RendererBinding]), but can be /// Typically created by the binding (e.g., [RendererBinding]), but can be
@ -984,7 +984,7 @@ class PipelineOwner {
return true; return true;
}()); }());
FlutterTimeline.startSync( FlutterTimeline.startSync(
'LAYOUT', 'LAYOUT$_debugRootSuffixForTimelineEventNames',
arguments: debugTimelineArguments, arguments: debugTimelineArguments,
); );
} }
@ -1071,7 +1071,7 @@ class PipelineOwner {
/// [flushPaint]. /// [flushPaint].
void flushCompositingBits() { void flushCompositingBits() {
if (!kReleaseMode) { if (!kReleaseMode) {
FlutterTimeline.startSync('UPDATING COMPOSITING BITS'); FlutterTimeline.startSync('UPDATING COMPOSITING BITS$_debugRootSuffixForTimelineEventNames');
} }
_nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth); _nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
for (final RenderObject node in _nodesNeedingCompositingBitsUpdate) { for (final RenderObject node in _nodesNeedingCompositingBitsUpdate) {
@ -1120,7 +1120,7 @@ class PipelineOwner {
return true; return true;
}()); }());
FlutterTimeline.startSync( FlutterTimeline.startSync(
'PAINT', 'PAINT$_debugRootSuffixForTimelineEventNames',
arguments: debugTimelineArguments, arguments: debugTimelineArguments,
); );
} }
@ -1247,7 +1247,7 @@ class PipelineOwner {
return; return;
} }
if (!kReleaseMode) { if (!kReleaseMode) {
FlutterTimeline.startSync('SEMANTICS'); FlutterTimeline.startSync('SEMANTICS$_debugRootSuffixForTimelineEventNames');
} }
assert(_semanticsOwner != null); assert(_semanticsOwner != null);
assert(() { assert(() {
@ -1279,6 +1279,20 @@ class PipelineOwner {
} }
} }
@override
List<DiagnosticsNode> debugDescribeChildren() {
return <DiagnosticsNode>[
for (final PipelineOwner child in _children)
child.toDiagnosticsNode(),
];
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<RenderObject>('rootNode', rootNode, defaultValue: null));
}
// TREE MANAGEMENT // TREE MANAGEMENT
final Set<PipelineOwner> _children = <PipelineOwner>{}; final Set<PipelineOwner> _children = <PipelineOwner>{};
@ -1290,6 +1304,8 @@ class PipelineOwner {
return true; return true;
} }
String get _debugRootSuffixForTimelineEventNames => _debugParent == null ? ' (root)' : '';
/// Mark this [PipelineOwner] as attached to the given [PipelineManifold]. /// Mark this [PipelineOwner] as attached to the given [PipelineManifold].
/// ///
/// Typically, this is only called directly on the root [PipelineOwner]. /// Typically, this is only called directly on the root [PipelineOwner].
@ -1315,7 +1331,9 @@ class PipelineOwner {
assert(_manifold != null); assert(_manifold != null);
_manifold!.removeListener(_updateSemanticsOwner); _manifold!.removeListener(_updateSemanticsOwner);
_manifold = null; _manifold = null;
_updateSemanticsOwner(); // Not updating the semantics owner here to not disrupt any of its clients
// in case we get re-attached. If necessary, semantics owner will be updated
// in "attach", or disposed in "dispose", if not reattached.
for (final PipelineOwner child in _children) { for (final PipelineOwner child in _children) {
child.detach(); child.detach();
@ -1351,7 +1369,9 @@ class PipelineOwner {
assert(!_children.contains(child)); assert(!_children.contains(child));
assert(_debugAllowChildListModifications, 'Cannot modify child list after layout.'); assert(_debugAllowChildListModifications, 'Cannot modify child list after layout.');
_children.add(child); _children.add(child);
assert(_debugSetParent(child, this)); if (!kReleaseMode) {
_debugSetParent(child, this);
}
if (_manifold != null) { if (_manifold != null) {
child.attach(_manifold!); child.attach(_manifold!);
} }
@ -1369,7 +1389,9 @@ class PipelineOwner {
assert(_children.contains(child)); assert(_children.contains(child));
assert(_debugAllowChildListModifications, 'Cannot modify child list after layout.'); assert(_debugAllowChildListModifications, 'Cannot modify child list after layout.');
_children.remove(child); _children.remove(child);
assert(_debugSetParent(child, null)); if (!kReleaseMode) {
_debugSetParent(child, null);
}
if (_manifold != null) { if (_manifold != null) {
child.detach(); child.detach();
} }
@ -1384,6 +1406,26 @@ class PipelineOwner {
void visitChildren(PipelineOwnerVisitor visitor) { void visitChildren(PipelineOwnerVisitor visitor) {
_children.forEach(visitor); _children.forEach(visitor);
} }
/// Release any resources held by this pipeline owner.
///
/// Prior to calling this method the pipeline owner must be removed from the
/// pipeline owner tree, i.e. it must have neither a parent nor any children
/// (see [dropChild]). It also must be [detach]ed from any [PipelineManifold].
///
/// The object is no longer usable after calling dispose.
void dispose() {
assert(_children.isEmpty);
assert(rootNode == null);
assert(_manifold == null);
assert(_debugParent == null);
_semanticsOwner?.dispose();
_semanticsOwner = null;
_nodesNeedingLayout.clear();
_nodesNeedingCompositingBitsUpdate.clear();
_nodesNeedingPaint.clear();
_nodesNeedingSemantics.clear();
}
} }
/// Signature for the callback to [PipelineOwner.visitChildren]. /// Signature for the callback to [PipelineOwner.visitChildren].
@ -3919,7 +3961,6 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
/// This mixin is typically used to implement render objects created /// This mixin is typically used to implement render objects created
/// in a [SingleChildRenderObjectWidget]. /// in a [SingleChildRenderObjectWidget].
mixin RenderObjectWithChildMixin<ChildType extends RenderObject> on RenderObject { mixin RenderObjectWithChildMixin<ChildType extends RenderObject> on RenderObject {
/// Checks whether the given render object has the correct [runtimeType] to be /// Checks whether the given render object has the correct [runtimeType] to be
/// a child of this render object. /// a child of this render object.
/// ///

View File

@ -67,10 +67,14 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
/// ///
/// Typically created by the binding (e.g., [RendererBinding]). /// Typically created by the binding (e.g., [RendererBinding]).
/// ///
/// The [configuration] must not be null. /// Providing a [configuration] is optional, but a configuration must be set
/// before calling [prepareInitialFrame]. This decouples creating the
/// [RenderView] object from configuring it. Typically, the object is created
/// by the [View] widget and configured by the [RendererBinding] when the
/// [RenderView] is registered with it by the [View] widget.
RenderView({ RenderView({
RenderBox? child, RenderBox? child,
required ViewConfiguration configuration, ViewConfiguration? configuration,
required ui.FlutterView view, required ui.FlutterView view,
}) : _configuration = configuration, }) : _configuration = configuration,
_view = view { _view = view {
@ -82,26 +86,39 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
Size _size = Size.zero; Size _size = Size.zero;
/// The constraints used for the root layout. /// The constraints used for the root layout.
ViewConfiguration get configuration => _configuration;
ViewConfiguration _configuration;
/// The configuration is initially set by the [configuration] argument
/// passed to the constructor.
/// ///
/// Always call [prepareInitialFrame] before changing the configuration. /// Typically, this configuration is set by the [RendererBinding], when the
/// [RenderView] is registered with it. It will also update the configuration
/// if necessary. Therefore, if used in conjunction with the [RendererBinding]
/// this property must not be set manually as the [RendererBinding] will just
/// override it.
///
/// For tests that want to change the size of the view, set
/// [TestFlutterView.physicalSize] on the appropriate [TestFlutterView]
/// (typically [WidgetTester.view]) instead of setting a configuration
/// directly on the [RenderView].
ViewConfiguration get configuration => _configuration!;
ViewConfiguration? _configuration;
set configuration(ViewConfiguration value) { set configuration(ViewConfiguration value) {
if (configuration == value) { if (_configuration == value) {
return; return;
} }
final ViewConfiguration oldConfiguration = _configuration; final ViewConfiguration? oldConfiguration = _configuration;
_configuration = value; _configuration = value;
if (oldConfiguration.toMatrix() != _configuration.toMatrix()) { if (_rootTransform == null) {
// [prepareInitialFrame] has not been called yet, nothing to do for now.
return;
}
if (oldConfiguration?.toMatrix() != configuration.toMatrix()) {
replaceRootLayer(_updateMatricesAndCreateNewRootLayer()); replaceRootLayer(_updateMatricesAndCreateNewRootLayer());
} }
assert(_rootTransform != null); assert(_rootTransform != null);
markNeedsLayout(); markNeedsLayout();
} }
/// Whether a [configuration] has been set.
bool get hasConfiguration => _configuration != null;
/// The [FlutterView] into which this [RenderView] will render. /// The [FlutterView] into which this [RenderView] will render.
ui.FlutterView get flutterView => _view; ui.FlutterView get flutterView => _view;
final ui.FlutterView _view; final ui.FlutterView _view;

View File

@ -0,0 +1,177 @@
// 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 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'framework.dart';
/// A bridge from a [RenderObject] to an [Element] tree.
///
/// The given container is the [RenderObject] that the [Element] tree should be
/// inserted into. It must be a [RenderObject] that implements the
/// [RenderObjectWithChildMixin] protocol. The type argument `T` is the kind of
/// [RenderObject] that the container expects as its child.
///
/// The [RenderObjectToWidgetAdapter] is an alternative to [RootWidget] for
/// bootstrapping an element tree. Unlike [RootWidget] it requires the
/// existence of a render tree (the [container]) to attach the element tree to.
class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWidget {
/// Creates a bridge from a [RenderObject] to an [Element] tree.
RenderObjectToWidgetAdapter({
this.child,
required this.container,
this.debugShortDescription,
}) : super(key: GlobalObjectKey(container));
/// The widget below this widget in the tree.
///
/// {@macro flutter.widgets.ProxyWidget.child}
final Widget? child;
/// The [RenderObject] that is the parent of the [Element] created by this widget.
final RenderObjectWithChildMixin<T> container;
/// A short description of this widget used by debugging aids.
final String? debugShortDescription;
@override
RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);
@override
RenderObjectWithChildMixin<T> createRenderObject(BuildContext context) => container;
@override
void updateRenderObject(BuildContext context, RenderObject renderObject) { }
/// Inflate this widget and actually set the resulting [RenderObject] as the
/// child of [container].
///
/// If `element` is null, this function will create a new element. Otherwise,
/// the given element will have an update scheduled to switch to this widget.
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T>? element ]) {
if (element == null) {
owner.lockState(() {
element = createElement();
assert(element != null);
element!.assignOwner(owner);
});
owner.buildScope(element!, () {
element!.mount(null, null);
});
} else {
element._newWidget = this;
element.markNeedsBuild();
}
return element!;
}
@override
String toStringShort() => debugShortDescription ?? super.toStringShort();
}
/// The root of an element tree that is hosted by a [RenderObject].
///
/// This element class is the instantiation of a [RenderObjectToWidgetAdapter]
/// widget. It can be used only as the root of an [Element] tree (it cannot be
/// mounted into another [Element]; it's parent must be null).
///
/// In typical usage, it will be instantiated for a [RenderObjectToWidgetAdapter]
/// whose container is the [RenderView].
class RenderObjectToWidgetElement<T extends RenderObject> extends RenderTreeRootElement with RootElementMixin {
/// Creates an element that is hosted by a [RenderObject].
///
/// The [RenderObject] created by this element is not automatically set as a
/// child of the hosting [RenderObject]. To actually attach this element to
/// the render tree, call [RenderObjectToWidgetAdapter.attachToRenderTree].
RenderObjectToWidgetElement(RenderObjectToWidgetAdapter<T> super.widget);
Element? _child;
static const Object _rootChildSlot = Object();
@override
void visitChildren(ElementVisitor visitor) {
if (_child != null) {
visitor(_child!);
}
}
@override
void forgetChild(Element child) {
assert(child == _child);
_child = null;
super.forgetChild(child);
}
@override
void mount(Element? parent, Object? newSlot) {
assert(parent == null);
super.mount(parent, newSlot);
_rebuild();
assert(_child != null);
}
@override
void update(RenderObjectToWidgetAdapter<T> newWidget) {
super.update(newWidget);
assert(widget == newWidget);
_rebuild();
}
// When we are assigned a new widget, we store it here
// until we are ready to update to it.
Widget? _newWidget;
@override
void performRebuild() {
if (_newWidget != null) {
// _newWidget can be null if, for instance, we were rebuilt
// due to a reassemble.
final Widget newWidget = _newWidget!;
_newWidget = null;
update(newWidget as RenderObjectToWidgetAdapter<T>);
}
super.performRebuild();
assert(_newWidget == null);
}
@pragma('vm:notify-debugger-on-exception')
void _rebuild() {
try {
_child = updateChild(_child, (widget as RenderObjectToWidgetAdapter<T>).child, _rootChildSlot);
} catch (exception, stack) {
final FlutterErrorDetails details = FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widgets library',
context: ErrorDescription('attaching to the render tree'),
);
FlutterError.reportError(details);
final Widget error = ErrorWidget.builder(details);
_child = updateChild(null, error, _rootChildSlot);
}
}
@override
RenderObjectWithChildMixin<T> get renderObject => super.renderObject as RenderObjectWithChildMixin<T>;
@override
void insertRenderObjectChild(RenderObject child, Object? slot) {
assert(slot == _rootChildSlot);
assert(renderObject.debugValidateChild(child));
renderObject.child = child as T;
}
@override
void moveRenderObjectChild(RenderObject child, Object? oldSlot, Object? newSlot) {
assert(false);
}
@override
void removeRenderObjectChild(RenderObject child, Object? slot) {
assert(renderObject.child == child);
renderObject.child = null;
}
}

View File

@ -280,6 +280,48 @@ abstract mixin class WidgetsBindingObserver {
} }
/// The glue between the widgets layer and the Flutter engine. /// The glue between the widgets layer and the Flutter engine.
///
/// The [WidgetsBinding] manages a single [Element] tree rooted at [rootElement].
/// Calling [runApp] (which indirectly calls [attachRootWidget]) bootstraps that
/// element tree.
///
/// ## Relationship to render trees
///
/// Multiple render trees may be associated with the element tree. Those are
/// managed by the underlying [RendererBinding].
///
/// The element tree is segmented into two types of zones: rendering zones and
/// non-rendering zones.
///
/// A rendering zone is a part of the element tree that is backed by a render
/// tree and it describes the pixels that are drawn on screen. For elements in
/// this zone, [Element.renderObject] never returns null because the elements
/// are all associated with [RenderObject]s. Almost all widgets can be placed in
/// a rendering zone; notable exceptions are the [View] widget, [ViewCollection]
/// widget, and [RootWidget].
///
/// A non-rendering zone is a part of the element tree that is not backed by a
/// render tree. For elements in this zone, [Element.renderObject] returns null
/// because the elements are not associated with any [RenderObject]s. Only
/// widgets that do not produce a [RenderObject] can be used in this zone
/// because there is no render tree to attach the render object to. In other
/// words, [RenderObjectWidget]s cannot be used in this zone. Typically, one
/// would find [InheritedWidget]s, [View]s, and [ViewCollection]s in this zone
/// to inject data across rendering zones into the tree and to organize the
/// rendering zones (and by extension their associated render trees) into a
/// unified element tree.
///
/// The root of the element tree at [rootElement] starts a non-rendering zone.
/// Within a non-rendering zone, the [View] widget is used to start a rendering
/// zone by bootstrapping a render tree. Within a rendering zone, the
/// [ViewAnchor] can be used to start a new non-rendering zone.
///
// TODO(goderbauer): Include an example graph showcasing the different zones.
///
/// To figure out if an element is in a rendering zone it may walk up the tree
/// calling [Element.debugExpectsRenderObjectForSlot] on its ancestors. If it
/// reaches an element that returns false, it is in a non-rendering zone. If it
/// reaches a [RenderObjectElement] ancestor it is in a rendering zone.
mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding { mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
@override @override
void initInstances() { void initInstances() {
@ -975,6 +1017,8 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
Widget wrapWithDefaultView(Widget rootWidget) { Widget wrapWithDefaultView(Widget rootWidget) {
return View( return View(
view: platformDispatcher.implicitView!, view: platformDispatcher.implicitView!,
deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: pipelineOwner,
deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: renderView,
child: rootWidget, child: rootWidget,
); );
} }
@ -1000,13 +1044,25 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
/// * [RenderObjectToWidgetAdapter.attachToRenderTree], which inflates a /// * [RenderObjectToWidgetAdapter.attachToRenderTree], which inflates a
/// widget and attaches it to the render tree. /// widget and attaches it to the render tree.
void attachRootWidget(Widget rootWidget) { void attachRootWidget(Widget rootWidget) {
final bool isBootstrapFrame = rootElement == null; attachToBuildOwner(RootWidget(
_readyToProduceFrames = true;
_rootElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]', debugShortDescription: '[root]',
child: rootWidget, child: rootWidget,
).attachToRenderTree(buildOwner!, rootElement as RenderObjectToWidgetElement<RenderBox>?); ));
}
/// Called by [attachRootWidget] to attach the provided [RootWidget] to the
/// [buildOwner].
///
/// This creates the [rootElement], if necessary, or re-uses an existing one.
///
/// This method is rarely called directly, but it can be useful in tests to
/// restore the element tree to a previous version by providing the
/// [RootWidget] of that version (see [WidgetTester.restartAndRestore] for an
/// exemplary use case).
void attachToBuildOwner(RootWidget widget) {
final bool isBootstrapFrame = rootElement == null;
_readyToProduceFrames = true;
_rootElement = widget.attach(buildOwner!, rootElement as RootElement?);
if (isBootstrapFrame) { if (isBootstrapFrame) {
SchedulerBinding.instance.ensureVisualUpdate(); SchedulerBinding.instance.ensureVisualUpdate();
} }
@ -1121,52 +1177,40 @@ void debugDumpApp() {
debugPrint(_debugDumpAppString()); debugPrint(_debugDumpAppString());
} }
/// A bridge from a [RenderObject] to an [Element] tree. /// A widget for the root of the widget tree.
/// ///
/// The given container is the [RenderObject] that the [Element] tree should be /// Exposes an [attach] method to attach the widget tree to a [BuildOwner]. That
/// inserted into. It must be a [RenderObject] that implements the /// method also bootstraps the element tree.
/// [RenderObjectWithChildMixin] protocol. The type argument `T` is the kind of
/// [RenderObject] that the container expects as its child.
/// ///
/// Used by [runApp] to bootstrap applications. /// Used by [WidgetsBinding.attachRootWidget] (which is indirectly called by
class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWidget { /// [runApp]) to bootstrap applications.
/// Creates a bridge from a [RenderObject] to an [Element] tree. class RootWidget extends Widget {
/// /// Creates a [RootWidget].
/// Used by [WidgetsBinding] to attach the root widget to the [RenderView]. const RootWidget({
RenderObjectToWidgetAdapter({ super.key,
this.child, this.child,
required this.container,
this.debugShortDescription, this.debugShortDescription,
}) : super(key: GlobalObjectKey(container)); });
/// The widget below this widget in the tree. /// The widget below this widget in the tree.
/// ///
/// {@macro flutter.widgets.ProxyWidget.child} /// {@macro flutter.widgets.ProxyWidget.child}
final Widget? child; final Widget? child;
/// The [RenderObject] that is the parent of the [Element] created by this widget.
final RenderObjectWithChildMixin<T> container;
/// A short description of this widget used by debugging aids. /// A short description of this widget used by debugging aids.
final String? debugShortDescription; final String? debugShortDescription;
@override @override
RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this); RootElement createElement() => RootElement(this);
@override /// Inflate this widget and attaches it to the provided [BuildOwner].
RenderObjectWithChildMixin<T> createRenderObject(BuildContext context) => container;
@override
void updateRenderObject(BuildContext context, RenderObject renderObject) { }
/// Inflate this widget and actually set the resulting [RenderObject] as the
/// child of [container].
/// ///
/// If `element` is null, this function will create a new element. Otherwise, /// If `element` is null, this function will create a new element. Otherwise,
/// the given element will have an update scheduled to switch to this widget. /// the given element will have an update scheduled to switch to this widget.
/// ///
/// Used by [runApp] to bootstrap applications. /// Used by [WidgetsBinding.attachToBuildOwner] (which is indirectly called by
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T>? element ]) { /// [runApp]) to bootstrap applications.
RootElement attach(BuildOwner owner, [ RootElement? element ]) {
if (element == null) { if (element == null) {
owner.lockState(() { owner.lockState(() {
element = createElement(); element = createElement();
@ -1174,7 +1218,7 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi
element!.assignOwner(owner); element!.assignOwner(owner);
}); });
owner.buildScope(element!, () { owner.buildScope(element!, () {
element!.mount(null, null); element!.mount(/* parent */ null, /* slot */ null);
}); });
} else { } else {
element._newWidget = this; element._newWidget = this;
@ -1187,28 +1231,22 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi
String toStringShort() => debugShortDescription ?? super.toStringShort(); String toStringShort() => debugShortDescription ?? super.toStringShort();
} }
/// The root of the element tree that is hosted by a [RenderObject]. /// The root of the element tree.
/// ///
/// This element class is the instantiation of a [RenderObjectToWidgetAdapter] /// This element class is the instantiation of a [RootWidget]. It can be used
/// widget. It can be used only as the root of an [Element] tree (it cannot be /// only as the root of an [Element] tree (it cannot be mounted into another
/// mounted into another [Element]; it's parent must be null). /// [Element]; its parent must be null).
/// ///
/// In typical usage, it will be instantiated for a [RenderObjectToWidgetAdapter] /// In typical usage, it will be instantiated for a [RootWidget] by calling
/// whose container is the [RenderView] that connects to the Flutter engine. In /// [RootWidget.attach]. In this usage, it is normally instantiated by the
/// this usage, it is normally instantiated by the bootstrapping logic in the /// bootstrapping logic in the [WidgetsFlutterBinding] singleton created by
/// [WidgetsFlutterBinding] singleton created by [runApp]. /// [runApp].
class RenderObjectToWidgetElement<T extends RenderObject> extends RenderObjectElement with RootElementMixin { class RootElement extends Element with RootElementMixin {
/// Creates an element that is hosted by a [RenderObject]. /// Creates a [RootElement] for the provided [RootWidget].
/// RootElement(RootWidget super.widget);
/// The [RenderObject] created by this element is not automatically set as a
/// child of the hosting [RenderObject]. To actually attach this element to
/// the render tree, call [RenderObjectToWidgetAdapter.attachToRenderTree].
RenderObjectToWidgetElement(RenderObjectToWidgetAdapter<T> super.widget);
Element? _child; Element? _child;
static const Object _rootChildSlot = Object();
@override @override
void visitChildren(ElementVisitor visitor) { void visitChildren(ElementVisitor visitor) {
if (_child != null) { if (_child != null) {
@ -1225,14 +1263,15 @@ class RenderObjectToWidgetElement<T extends RenderObject> extends RenderObjectEl
@override @override
void mount(Element? parent, Object? newSlot) { void mount(Element? parent, Object? newSlot) {
assert(parent == null); assert(parent == null); // We are the root!
super.mount(parent, newSlot); super.mount(parent, newSlot);
_rebuild(); _rebuild();
assert(_child != null); assert(_child != null);
super.performRebuild(); // clears the "dirty" flag
} }
@override @override
void update(RenderObjectToWidgetAdapter<T> newWidget) { void update(RootWidget newWidget) {
super.update(newWidget); super.update(newWidget);
assert(widget == newWidget); assert(widget == newWidget);
_rebuild(); _rebuild();
@ -1240,25 +1279,24 @@ class RenderObjectToWidgetElement<T extends RenderObject> extends RenderObjectEl
// When we are assigned a new widget, we store it here // When we are assigned a new widget, we store it here
// until we are ready to update to it. // until we are ready to update to it.
Widget? _newWidget; RootWidget? _newWidget;
@override @override
void performRebuild() { void performRebuild() {
if (_newWidget != null) { if (_newWidget != null) {
// _newWidget can be null if, for instance, we were rebuilt // _newWidget can be null if, for instance, we were rebuilt
// due to a reassemble. // due to a reassemble.
final Widget newWidget = _newWidget!; final RootWidget newWidget = _newWidget!;
_newWidget = null; _newWidget = null;
update(newWidget as RenderObjectToWidgetAdapter<T>); update(newWidget);
} }
super.performRebuild(); super.performRebuild();
assert(_newWidget == null); assert(_newWidget == null);
} }
@pragma('vm:notify-debugger-on-exception')
void _rebuild() { void _rebuild() {
try { try {
_child = updateChild(_child, (widget as RenderObjectToWidgetAdapter<T>).child, _rootChildSlot); _child = updateChild(_child, (widget as RootWidget).child, /* slot */ null);
} catch (exception, stack) { } catch (exception, stack) {
final FlutterErrorDetails details = FlutterErrorDetails( final FlutterErrorDetails details = FlutterErrorDetails(
exception: exception, exception: exception,
@ -1267,31 +1305,18 @@ class RenderObjectToWidgetElement<T extends RenderObject> extends RenderObjectEl
context: ErrorDescription('attaching to the render tree'), context: ErrorDescription('attaching to the render tree'),
); );
FlutterError.reportError(details); FlutterError.reportError(details);
final Widget error = ErrorWidget.builder(details); // No error widget possible here since it wouldn't have a view to render into.
_child = updateChild(null, error, _rootChildSlot); _child = null;
} }
} }
@override @override
RenderObjectWithChildMixin<T> get renderObject => super.renderObject as RenderObjectWithChildMixin<T>; bool get debugDoingBuild => false; // This element doesn't have a build phase.
@override @override
void insertRenderObjectChild(RenderObject child, Object? slot) { // There is no ancestor RenderObjectElement that the render object could be attached to.
assert(slot == _rootChildSlot); bool debugExpectsRenderObjectForSlot(Object? slot) => false;
assert(renderObject.debugValidateChild(child));
renderObject.child = child as T;
}
@override
void moveRenderObjectChild(RenderObject child, Object? oldSlot, Object? newSlot) {
assert(false);
}
@override
void removeRenderObjectChild(RenderObject child, Object? slot) {
assert(renderObject.child == child);
renderObject.child = null;
}
} }
/// A concrete binding for applications based on the Widgets framework. /// A concrete binding for applications based on the Widgets framework.

View File

@ -3452,6 +3452,11 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
/// If this object is a [RenderObjectElement], the render object is the one at /// If this object is a [RenderObjectElement], the render object is the one at
/// this location in the tree. Otherwise, this getter will walk down the tree /// this location in the tree. Otherwise, this getter will walk down the tree
/// until it finds a [RenderObjectElement]. /// until it finds a [RenderObjectElement].
///
/// Some locations in the tree are not backed by a render object. In those
/// cases, this getter returns null. This can happen, if the element is
/// located outside of a [View] since only the element subtree rooted in a
/// view has a render tree associated with it.
RenderObject? get renderObject { RenderObject? get renderObject {
Element? current = this; Element? current = this;
while (current != null) { while (current != null) {
@ -3460,17 +3465,33 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
} else if (current is RenderObjectElement) { } else if (current is RenderObjectElement) {
return current.renderObject; return current.renderObject;
} else { } else {
Element? next; current = current.renderObjectAttachingChild;
current.visitChildren((Element child) {
assert(next == null); // This verifies that there's only one child.
next = child;
});
current = next;
} }
} }
return null; return null;
} }
/// Returns the child of this [Element] that will insert a [RenderObject] into
/// an ancestor of this Element to construct the render tree.
///
/// Returns null if this Element doesn't have any children who need to attach
/// a [RenderObject] to an ancestor of this [Element]. A [RenderObjectElement]
/// will therefore return null because its children insert their
/// [RenderObject]s into the [RenderObjectElement] itself and not into an
/// ancestor of the [RenderObjectElement].
///
/// Furthermore, this may return null for [Element]s that hoist their own
/// independent render tree and do not extend the ancestor render tree.
@protected
Element? get renderObjectAttachingChild {
Element? next;
visitChildren((Element child) {
assert(next == null); // This verifies that there's only one child.
next = child;
});
return next;
}
@override @override
List<DiagnosticsNode> describeMissingAncestor({ required Type expectedAncestorType }) { List<DiagnosticsNode> describeMissingAncestor({ required Type expectedAncestorType }) {
final List<DiagnosticsNode> information = <DiagnosticsNode>[]; final List<DiagnosticsNode> information = <DiagnosticsNode>[];
@ -4021,15 +4042,20 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
assert(_lifecycleState == _ElementLifecycle.active); assert(_lifecycleState == _ElementLifecycle.active);
assert(child._parent == this); assert(child._parent == this);
void visit(Element element) { void visit(Element element) {
element._updateSlot(newSlot); element.updateSlot(newSlot);
if (element is! RenderObjectElement) { final Element? descendant = element.renderObjectAttachingChild;
element.visitChildren(visit); if (descendant != null) {
visit(descendant);
} }
} }
visit(child); visit(child);
} }
void _updateSlot(Object? newSlot) { /// Called by [updateSlotForChild] when the framework needs to change the slot
/// that this [Element] occupies in its ancestor.
@protected
@mustCallSuper
void updateSlot(Object? newSlot) {
assert(_lifecycleState == _ElementLifecycle.active); assert(_lifecycleState == _ElementLifecycle.active);
assert(_parent != null); assert(_parent != null);
assert(_parent!._lifecycleState == _ElementLifecycle.active); assert(_parent!._lifecycleState == _ElementLifecycle.active);
@ -4070,7 +4096,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
/// ///
/// The `newSlot` argument specifies the new value for this element's [slot]. /// The `newSlot` argument specifies the new value for this element's [slot].
void attachRenderObject(Object? newSlot) { void attachRenderObject(Object? newSlot) {
assert(_slot == null); assert(slot == null);
visitChildren((Element child) { visitChildren((Element child) {
child.attachRenderObject(newSlot); child.attachRenderObject(newSlot);
}); });
@ -4143,7 +4169,6 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
@protected @protected
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
Element inflateWidget(Widget newWidget, Object? newSlot) { Element inflateWidget(Widget newWidget, Object? newSlot) {
final bool isTimelineTracked = !kReleaseMode && _isProfileBuildsEnabledFor(newWidget); final bool isTimelineTracked = !kReleaseMode && _isProfileBuildsEnabledFor(newWidget);
if (isTimelineTracked) { if (isTimelineTracked) {
Map<String, String>? debugTimelineArguments; Map<String, String>? debugTimelineArguments;
@ -4169,7 +4194,17 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
_debugCheckForCycles(newChild); _debugCheckForCycles(newChild);
return true; return true;
}()); }());
try {
newChild._activateWithParent(this, newSlot); newChild._activateWithParent(this, newSlot);
} catch (_) {
// Attempt to do some clean-up if activation fails to leave tree in a reasonable state.
try {
deactivateChild(newChild);
} catch (_) {
// Clean-up failed. Only surface original exception.
}
rethrow;
}
final Element? updatedChild = updateChild(newChild, newWidget, newSlot); final Element? updatedChild = updateChild(newChild, newWidget, newSlot);
assert(newChild == updatedChild); assert(newChild == updatedChild);
return updatedChild!; return updatedChild!;
@ -4404,6 +4439,33 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
_lifecycleState = _ElementLifecycle.defunct; _lifecycleState = _ElementLifecycle.defunct;
} }
/// Whether the child in the provided `slot` (or one of its descendants) must
/// insert a [RenderObject] into its ancestor [RenderObjectElement] by calling
/// [RenderObjectElement.insertRenderObjectChild] on it.
///
/// This method is used to define non-rendering zones in the element tree (see
/// [WidgetsBinding] for an explanation of rendering and non-rendering zones):
///
/// Most branches of the [Element] tree are expected to eventually insert a
/// [RenderObject] into their [RenderObjectElement] ancestor to construct the
/// render tree. However, there is a notable exception: an [Element] may
/// expect that the occupant of a certain child slot creates a new independent
/// render tree and therefore is not allowed to insert a render object into
/// the existing render tree. Those elements must return false from this
/// method for the slot in question to signal to the child in that slot that
/// it must not call [RenderObjectElement.insertRenderObjectChild] on its
/// ancestor.
///
/// As an example, the element backing the [ViewAnchor] returns false from
/// this method for the [ViewAnchor.view] slot to enforce that it is occupied
/// by e.g. a [View] widget, which will ultimately bootstrap a separate
/// render tree for that view. Another example is the [ViewCollection] widget,
/// which returns false for all its slots for the same reason.
///
/// Overriding this method is not common, as elements behaving in the way
/// described above are rare.
bool debugExpectsRenderObjectForSlot(Object? slot) => true;
@override @override
RenderObject? findRenderObject() { RenderObject? findRenderObject() {
assert(() { assert(() {
@ -5266,6 +5328,9 @@ abstract class ComponentElement extends Element {
@override @override
bool get debugDoingBuild => _debugDoingBuild; bool get debugDoingBuild => _debugDoingBuild;
@override
Element? get renderObjectAttachingChild => _child;
@override @override
void mount(Element? parent, Object? newSlot) { void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot); super.mount(parent, newSlot);
@ -6073,6 +6138,9 @@ abstract class RenderObjectElement extends Element {
} }
RenderObject? _renderObject; RenderObject? _renderObject;
@override
Element? get renderObjectAttachingChild => null;
bool _debugDoingBuild = false; bool _debugDoingBuild = false;
@override @override
bool get debugDoingBuild => _debugDoingBuild; bool get debugDoingBuild => _debugDoingBuild;
@ -6082,8 +6150,25 @@ abstract class RenderObjectElement extends Element {
RenderObjectElement? _findAncestorRenderObjectElement() { RenderObjectElement? _findAncestorRenderObjectElement() {
Element? ancestor = _parent; Element? ancestor = _parent;
while (ancestor != null && ancestor is! RenderObjectElement) { while (ancestor != null && ancestor is! RenderObjectElement) {
ancestor = ancestor._parent; // In debug mode we check whether the ancestor accepts RenderObjects to
// produce a better error message in attachRenderObject. In release mode,
// we assume only correct trees are built (i.e.
// debugExpectsRenderObjectForSlot always returns true) and don't check
// explicitly.
assert(() {
if (!ancestor!.debugExpectsRenderObjectForSlot(slot)) {
ancestor = null;
} }
return true;
}());
ancestor = ancestor?._parent;
}
assert(() {
if (ancestor?.debugExpectsRenderObjectForSlot(slot) == false) {
ancestor = null;
}
return true;
}());
return ancestor as RenderObjectElement?; return ancestor as RenderObjectElement?;
} }
@ -6151,7 +6236,7 @@ abstract class RenderObjectElement extends Element {
_debugUpdateRenderObjectOwner(); _debugUpdateRenderObjectOwner();
return true; return true;
}()); }());
assert(_slot == newSlot); assert(slot == newSlot);
attachRenderObject(newSlot); attachRenderObject(newSlot);
super.performRebuild(); // clears the "dirty" flag super.performRebuild(); // clears the "dirty" flag
} }
@ -6252,12 +6337,13 @@ abstract class RenderObjectElement extends Element {
} }
@override @override
void _updateSlot(Object? newSlot) { void updateSlot(Object? newSlot) {
final Object? oldSlot = slot; final Object? oldSlot = slot;
assert(oldSlot != newSlot); assert(oldSlot != newSlot);
super._updateSlot(newSlot); super.updateSlot(newSlot);
assert(slot == newSlot); assert(slot == newSlot);
_ancestorRenderObjectElement!.moveRenderObjectChild(renderObject, oldSlot, slot); assert(_ancestorRenderObjectElement == _findAncestorRenderObjectElement());
_ancestorRenderObjectElement?.moveRenderObjectChild(renderObject, oldSlot, slot);
} }
@override @override
@ -6265,6 +6351,25 @@ abstract class RenderObjectElement extends Element {
assert(_ancestorRenderObjectElement == null); assert(_ancestorRenderObjectElement == null);
_slot = newSlot; _slot = newSlot;
_ancestorRenderObjectElement = _findAncestorRenderObjectElement(); _ancestorRenderObjectElement = _findAncestorRenderObjectElement();
assert(() {
if (_ancestorRenderObjectElement == null) {
FlutterError.reportError(FlutterErrorDetails(exception: FlutterError.fromParts(
<DiagnosticsNode>[
ErrorSummary(
'The render object for ${toStringShort()} cannot find ancestor render object to attach to.',
),
ErrorDescription(
'The ownership chain for the RenderObject in question was:\n ${debugGetCreatorChain(10)}',
),
ErrorHint(
'Try wrapping your widget in a View widget or any other widget that is backed by '
'a $RenderTreeRootElement to serve as the root of the render tree.',
),
]
)));
}
return true;
}());
_ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot); _ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot);
final ParentDataElement<ParentData>? parentDataElement = _findAncestorParentDataElement(); final ParentDataElement<ParentData>? parentDataElement = _findAncestorParentDataElement();
if (parentDataElement != null) { if (parentDataElement != null) {
@ -6597,6 +6702,67 @@ class MultiChildRenderObjectElement extends RenderObjectElement {
} }
} }
/// A [RenderObjectElement] used to manage the root of a render tree.
///
/// Unlike any other render object element this element does not attempt to
/// attach its [renderObject] to the closest ancestor [RenderObjectElement].
/// Instead, subclasses must override [attachRenderObject] and
/// [detachRenderObject] to attach/detach the [renderObject] to whatever
/// instance manages the render tree (e.g. by assigning it to
/// [PipelineOwner.rootNode]).
abstract class RenderTreeRootElement extends RenderObjectElement {
/// Creates an element that uses the given widget as its configuration.
RenderTreeRootElement(super.widget);
@override
@mustCallSuper
void attachRenderObject(Object? newSlot) {
_slot = newSlot;
assert(_debugCheckMustNotAttachRenderObjectToAncestor());
}
@override
@mustCallSuper
void detachRenderObject() {
_slot = null;
}
@override
void updateSlot(Object? newSlot) {
super.updateSlot(newSlot);
assert(_debugCheckMustNotAttachRenderObjectToAncestor());
}
bool _debugCheckMustNotAttachRenderObjectToAncestor() {
if (!kDebugMode) {
return true;
}
if (_findAncestorRenderObjectElement() != null) {
throw FlutterError.fromParts(
<DiagnosticsNode>[
ErrorSummary(
'The RenderObject for ${toStringShort()} cannot maintain an independent render tree at its current location.',
),
ErrorDescription(
'The ownership chain for the RenderObject in question was:\n ${debugGetCreatorChain(10)}',
),
ErrorDescription(
'This RenderObject is the root of an independent render tree and it cannot '
'attach itself to an ancestor in an existing tree. The ancestor RenderObject, '
'however, expects that a child will be attached.',
),
ErrorHint(
'Try moving the subtree that contains the ${toStringShort()} widget into the '
'view property of a ViewAnchor widget or to the root of the widget tree, where '
'it is not expected to attach its RenderObject to a parent.',
),
],
);
}
return true;
}
}
/// A wrapper class for the [Element] that is the creator of a [RenderObject]. /// A wrapper class for the [Element] that is the creator of a [RenderObject].
/// ///
/// Setting a [DebugCreator] as [RenderObject.debugCreator] will lead to better /// Setting a [DebugCreator] as [RenderObject.debugCreator] will lead to better

View File

@ -47,25 +47,31 @@ class SemanticsDebugger extends StatefulWidget {
} }
class _SemanticsDebuggerState extends State<SemanticsDebugger> with WidgetsBindingObserver { class _SemanticsDebuggerState extends State<SemanticsDebugger> with WidgetsBindingObserver {
late _SemanticsClient _client; _SemanticsClient? _client;
PipelineOwner? _pipelineOwner;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// TODO(abarth): We shouldn't reach out to the WidgetsBinding.instance
// static here because we might not be in a tree that's attached to that
// binding. Instead, we should find a way to get to the PipelineOwner from
// the BuildContext.
_client = _SemanticsClient(WidgetsBinding.instance.pipelineOwner)
..addListener(_update);
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
} }
@override
@override
void didChangeDependencies() {
super.didChangeDependencies();
final PipelineOwner newOwner = View.pipelineOwnerOf(context);
if (newOwner != _pipelineOwner) {
_client?.dispose();
_client = _SemanticsClient(newOwner)
..addListener(_update);
_pipelineOwner = newOwner;
}
}
@override @override
void dispose() { void dispose() {
_client _client?.dispose();
..removeListener(_update)
..dispose();
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
} }
@ -145,19 +151,15 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> with WidgetsBindi
} }
void _performAction(Offset position, SemanticsAction action) { void _performAction(Offset position, SemanticsAction action) {
_pipelineOwner.semanticsOwner?.performActionAt(position, action); _pipelineOwner?.semanticsOwner?.performActionAt(position, action);
} }
// TODO(abarth): This shouldn't be a static. We should get the pipeline owner
// from [context] somehow.
PipelineOwner get _pipelineOwner => WidgetsBinding.instance.pipelineOwner;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CustomPaint( return CustomPaint(
foregroundPainter: _SemanticsDebuggerPainter( foregroundPainter: _SemanticsDebuggerPainter(
_pipelineOwner, _pipelineOwner!,
_client.generation, _client!.generation,
_lastPointerDownLocation, // in physical pixels _lastPointerDownLocation, // in physical pixels
View.of(context).devicePixelRatio, View.of(context).devicePixelRatio,
widget.labelStyle, widget.labelStyle,

View File

@ -2,48 +2,89 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' show FlutterView; import 'dart:collection';
import 'dart:ui' show FlutterView, SemanticsUpdate;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'framework.dart'; import 'framework.dart';
import 'lookup_boundary.dart'; import 'lookup_boundary.dart';
import 'media_query.dart'; import 'media_query.dart';
/// Injects a [FlutterView] into the tree and makes it available to descendants /// Bootstraps a render tree that is rendered into the provided [FlutterView].
/// within the same [LookupBoundary] via [View.of] and [View.maybeOf]. ///
/// The content rendered into that view is determined by the provided [child].
/// Descendants within the same [LookupBoundary] can look up the view they are
/// rendered into via [View.of] and [View.maybeOf].
/// ///
/// The provided [child] is wrapped in a [MediaQuery] constructed from the given /// The provided [child] is wrapped in a [MediaQuery] constructed from the given
/// [view]. /// [view].
/// ///
/// In a future version of Flutter, the functionality of this widget will be
/// extended to actually bootstrap the render tree that is going to be rendered
/// into the provided [view]. This will enable rendering content into multiple
/// [FlutterView]s from a single widget tree.
///
/// Each [FlutterView] can be associated with at most one [View] widget in the /// Each [FlutterView] can be associated with at most one [View] widget in the
/// widget tree. Two or more [View] widgets configured with the same /// widget tree. Two or more [View] widgets configured with the same
/// [FlutterView] must never exist within the same widget tree at the same time. /// [FlutterView] must never exist within the same widget tree at the same time.
/// Internally, this limitation is enforced by a [GlobalObjectKey] that derives /// This limitation is enforced by a [GlobalObjectKey] that derives its identity
/// its identity from the [view] provided to this widget. /// from the [view] provided to this widget.
///
/// Since the [View] widget bootstraps its own independent render tree, neither
/// it nor any of its descendants will insert a [RenderObject] into an existing
/// render tree. Therefore, the [View] widget can only be used in those parts of
/// the widget tree where it is not required to participate in the construction
/// of the surrounding render tree. In other words, the widget may only be used
/// in a non-rendering zone of the widget tree (see [WidgetsBinding] for a
/// definition of rendering and non-rendering zones).
///
/// In practical terms, the widget is typically used at the root of the widget
/// tree outside of any other [View] widget, as a child of a [ViewCollection]
/// widget, or in the [ViewAnchor.view] slot of a [ViewAnchor] widget. It is not
/// required to be a direct child, though, since other non-[RenderObjectWidget]s
/// (e.g. [InheritedWidget]s, [Builder]s, or [StatefulWidget]s/[StatelessWidget]
/// that only produce non-[RenderObjectWidget]s) are allowed to be present
/// between those widgets and the [View] widget.
///
/// See also:
///
/// * [Element.debugExpectsRenderObjectForSlot], which defines whether a [View]
/// widget is allowed in a given child slot.
class View extends StatelessWidget { class View extends StatelessWidget {
/// Injects the provided [view] into the widget tree. /// Create a [View] widget to bootstrap a render tree that is rendered into
View({required this.view, required this.child}) : super(key: GlobalObjectKey(view)); /// the provided [FlutterView].
///
/// The content rendered into that [view] is determined by the given [child]
/// widget.
View({
super.key,
required this.view,
@Deprecated(
'Do not use. '
'This parameter only exists to implement the deprecated RendererBinding.pipelineOwner property until it is removed. '
'This feature was deprecated after v3.10.0-12.0.pre.'
)
PipelineOwner? deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner,
@Deprecated(
'Do not use. '
'This parameter only exists to implement the deprecated RendererBinding.renderView property until it is removed. '
'This feature was deprecated after v3.10.0-12.0.pre.'
)
RenderView? deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView,
required this.child,
}) : _deprecatedPipelineOwner = deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner,
_deprecatedRenderView = deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView,
assert((deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner == null) == (deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView == null)),
assert(deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView == null || deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView.flutterView == view);
/// The [FlutterView] to be injected into the tree. /// The [FlutterView] into which [child] is drawn.
final FlutterView view; final FlutterView view;
/// The widget below this widget in the tree, which will be drawn into the
/// [view].
///
/// {@macro flutter.widgets.ProxyWidget.child} /// {@macro flutter.widgets.ProxyWidget.child}
final Widget child; final Widget child;
@override final PipelineOwner? _deprecatedPipelineOwner;
Widget build(BuildContext context) { final RenderView? _deprecatedRenderView;
return _ViewScope(
view: view,
child: MediaQuery.fromView(
view: view,
child: child,
),
);
}
/// Returns the [FlutterView] that the provided `context` will render into. /// Returns the [FlutterView] that the provided `context` will render into.
/// ///
@ -106,13 +147,588 @@ class View extends StatelessWidget {
}()); }());
return result!; return result!;
} }
/// Returns the [PipelineOwner] parent to which a child [View] should attach
/// its [PipelineOwner] to.
///
/// If `context` has a [View] ancestor, it returns the [PipelineOwner]
/// responsible for managing the render tree of that view. If there is no
/// [View] ancestor, [RendererBinding.rootPipelineOwner] is returned instead.
static PipelineOwner pipelineOwnerOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<_PipelineOwnerScope>()?.pipelineOwner
?? RendererBinding.instance.rootPipelineOwner;
}
@override
Widget build(BuildContext context) {
return _RawView(
view: view,
deprecatedPipelineOwner: _deprecatedPipelineOwner,
deprecatedRenderView: _deprecatedRenderView,
builder: (BuildContext context, PipelineOwner owner) {
return _ViewScope(
view: view,
child: _PipelineOwnerScope(
pipelineOwner: owner,
child: MediaQuery.fromView(
view: view,
child: child,
),
),
);
}
);
}
}
/// A builder for the content [Widget] of a [_RawView].
///
/// The widget returned by the builder defines the content that is drawn into
/// the [FlutterView] configured on the [_RawView].
///
/// The builder is given the [PipelineOwner] that the [_RawView] uses to manage
/// its render tree. Typical builder implementations make that pipeline owner
/// available as an attachment point for potential child views.
///
/// Used by [_RawView.builder].
typedef _RawViewContentBuilder = Widget Function(BuildContext context, PipelineOwner owner);
/// The workhorse behind the [View] widget that actually bootstraps a render
/// tree.
///
/// It instantiates the [RenderView] as the root of that render tree and adds it
/// to the [RendererBinding] via [RendererBinding.addRenderView]. It also owns
/// the [PipelineOwner] that manages this render tree and adds it as a child to
/// the surrounding parent [PipelineOwner] obtained with [View.pipelineOwnerOf].
/// This ensures that the render tree bootstrapped by this widget participates
/// properly in frame production and hit testing.
class _RawView extends RenderObjectWidget {
/// Create a [RawView] widget to bootstrap a render tree that is rendered into
/// the provided [FlutterView].
///
/// The content rendered into that [view] is determined by the [Widget]
/// returned by [builder].
_RawView({
required this.view,
required PipelineOwner? deprecatedPipelineOwner,
required RenderView? deprecatedRenderView,
required this.builder,
}) : _deprecatedPipelineOwner = deprecatedPipelineOwner,
_deprecatedRenderView = deprecatedRenderView,
assert(deprecatedRenderView == null || deprecatedRenderView.flutterView == view),
// TODO(goderbauer): Replace this with GlobalObjectKey(view) when the deprecated properties are removed.
super(key: _DeprecatedRawViewKey(view, deprecatedPipelineOwner, deprecatedRenderView));
/// The [FlutterView] into which the [Widget] returned by [builder] is drawn.
final FlutterView view;
/// Determines the content [Widget] that is drawn into the [view].
///
/// The [builder] is given the [PipelineOwner] responsible for the render tree
/// bootstrapped by this widget and typically makes it available as an
/// attachment point for potential child views.
final _RawViewContentBuilder builder;
final PipelineOwner? _deprecatedPipelineOwner;
final RenderView? _deprecatedRenderView;
@override
RenderObjectElement createElement() => _RawViewElement(this);
@override
RenderObject createRenderObject(BuildContext context) {
return _deprecatedRenderView ?? RenderView(
view: view,
);
}
// No need to implement updateRenderObject: RawView uses the view as a
// GlobalKey, so we never need to update the RenderObject with a new view.
}
class _RawViewElement extends RenderTreeRootElement {
_RawViewElement(super.widget);
late final PipelineOwner _pipelineOwner = PipelineOwner(
onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,
onSemanticsUpdate: _handleSemanticsUpdate,
onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,
);
PipelineOwner get _effectivePipelineOwner => (widget as _RawView)._deprecatedPipelineOwner ?? _pipelineOwner;
void _handleSemanticsOwnerCreated() {
(_effectivePipelineOwner.rootNode as RenderView?)?.scheduleInitialSemantics();
}
void _handleSemanticsOwnerDisposed() {
(_effectivePipelineOwner.rootNode as RenderView?)?.clearSemantics();
}
void _handleSemanticsUpdate(SemanticsUpdate update) {
(widget as _RawView).view.updateSemantics(update);
}
@override
RenderView get renderObject => super.renderObject as RenderView;
Element? _child;
void _updateChild() {
try {
final Widget child = (widget as _RawView).builder(this, _effectivePipelineOwner);
_child = updateChild(_child, child, null);
} catch (e, stack) {
final FlutterErrorDetails details = FlutterErrorDetails(
exception: e,
stack: stack,
library: 'widgets library',
context: ErrorDescription('building $this'),
informationCollector: !kDebugMode ? null : () => <DiagnosticsNode>[
DiagnosticsDebugCreator(DebugCreator(this)),
],
);
FlutterError.reportError(details);
final Widget error = ErrorWidget.builder(details);
_child = updateChild(null, error, slot);
}
}
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
assert(_effectivePipelineOwner.rootNode == null);
_effectivePipelineOwner.rootNode = renderObject;
_attachView();
_updateChild();
renderObject.prepareInitialFrame();
if (_effectivePipelineOwner.semanticsOwner != null) {
renderObject.scheduleInitialSemantics();
}
}
PipelineOwner? _parentPipelineOwner; // Is null if view is currently not attached.
void _attachView([PipelineOwner? parentPipelineOwner]) {
assert(_parentPipelineOwner == null);
parentPipelineOwner ??= View.pipelineOwnerOf(this);
parentPipelineOwner.adoptChild(_effectivePipelineOwner);
RendererBinding.instance.addRenderView(renderObject);
_parentPipelineOwner = parentPipelineOwner;
}
void _detachView() {
final PipelineOwner? parentPipelineOwner = _parentPipelineOwner;
if (parentPipelineOwner != null) {
RendererBinding.instance.removeRenderView(renderObject);
parentPipelineOwner.dropChild(_effectivePipelineOwner);
_parentPipelineOwner = null;
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_parentPipelineOwner == null) {
return;
}
final PipelineOwner newParentPipelineOwner = View.pipelineOwnerOf(this);
if (newParentPipelineOwner != _parentPipelineOwner) {
_detachView();
_attachView(newParentPipelineOwner);
}
}
@override
void performRebuild() {
super.performRebuild();
_updateChild();
}
@override
void activate() {
super.activate();
assert(_effectivePipelineOwner.rootNode == null);
_effectivePipelineOwner.rootNode = renderObject;
_attachView();
}
@override
void deactivate() {
_detachView();
assert(_effectivePipelineOwner.rootNode == renderObject);
_effectivePipelineOwner.rootNode = null; // To satisfy the assert in the super class.
super.deactivate();
}
@override
void update(_RawView newWidget) {
super.update(newWidget);
_updateChild();
}
@override
void visitChildren(ElementVisitor visitor) {
if (_child != null) {
visitor(_child!);
}
}
@override
void forgetChild(Element child) {
assert(child == _child);
_child = null;
super.forgetChild(child);
}
@override
void insertRenderObjectChild(RenderBox child, Object? slot) {
assert(slot == null);
assert(renderObject.debugValidateChild(child));
renderObject.child = child;
}
@override
void moveRenderObjectChild(RenderObject child, Object? oldSlot, Object? newSlot) {
assert(false);
}
@override
void removeRenderObjectChild(RenderObject child, Object? slot) {
assert(slot == null);
assert(renderObject.child == child);
renderObject.child = null;
}
@override
void unmount() {
if (_effectivePipelineOwner != (widget as _RawView)._deprecatedPipelineOwner) {
_effectivePipelineOwner.dispose();
}
super.unmount();
}
} }
class _ViewScope extends InheritedWidget { class _ViewScope extends InheritedWidget {
const _ViewScope({required this.view, required super.child}); const _ViewScope({required this.view, required super.child});
final FlutterView view; final FlutterView? view;
@override @override
bool updateShouldNotify(_ViewScope oldWidget) => view != oldWidget.view; bool updateShouldNotify(_ViewScope oldWidget) => view != oldWidget.view;
} }
class _PipelineOwnerScope extends InheritedWidget {
const _PipelineOwnerScope({
required this.pipelineOwner,
required super.child,
});
final PipelineOwner pipelineOwner;
@override
bool updateShouldNotify(_PipelineOwnerScope oldWidget) => pipelineOwner != oldWidget.pipelineOwner;
}
class _MultiChildComponentWidget extends Widget {
const _MultiChildComponentWidget({
super.key,
List<Widget> views = const <Widget>[],
Widget? child,
}) : _views = views, _child = child;
// It is up to the subclasses to make the relevant properties public.
final List<Widget> _views;
final Widget? _child;
@override
Element createElement() => _MultiChildComponentElement(this);
}
/// A collection of sibling [View]s.
///
/// This widget can only be used in places were a [View] widget is allowed, i.e.
/// in a non-rendering zone of the widget tree. In practical terms, it can be
/// used at the root of the widget tree outside of any [View] widget, as a child
/// to a another [ViewCollection], or in the [ViewAnchor.view] slot of a
/// [ViewAnchor] widget. It is not required to be a direct child of those
/// widgets; other non-[RenderObjectWidget]s may appear in between the two (such
/// as an [InheritedWidget]).
///
/// Similarly, the [views] children of this widget must be [View]s, but they
/// may be wrapped in additional non-[RenderObjectWidget]s (e.g.
/// [InheritedWidget]s).
///
/// See also:
///
/// * [WidgetsBinding] for an explanation of rendering and non-rendering zones.
class ViewCollection extends _MultiChildComponentWidget {
/// Creates a [ViewCollection] widget.
///
/// The provided list of [views] must contain at least one widget.
const ViewCollection({super.key, required super.views}) : assert(views.length > 0);
/// The [View] descendants of this widget.
///
/// The [View]s may be wrapped in other non-[RenderObjectWidget]s (e.g.
/// [InheritedWidget]s). However, no [RenderObjectWidget] is allowed to appear
/// between the [ViewCollection] and the next [View] widget.
List<Widget> get views => _views;
}
/// Decorates a [child] widget with a side [View].
///
/// This widget must have a [View] ancestor, into which the [child] widget
/// is rendered.
///
/// Typically, a [View] or [ViewCollection] widget is used in the [view] slot to
/// define the content of the side view(s). Those widgets may be wrapped in
/// other non-[RenderObjectWidget]s (e.g. [InheritedWidget]s). However, no
/// [RenderObjectWidget] is allowed to appear between the [ViewAnchor] and the
/// next [View] widget in the [view] slot. The widgets in the [view] slot have
/// access to all [InheritedWidget]s above the [ViewAnchor] in the tree.
///
/// In technical terms, the [ViewAnchor] can only be used in a rendering zone of
/// the widget tree and the [view] slot marks the start of a new non-rendering
/// zone (see [WidgetsBinding] for a definition of these zones). Typically,
/// it is occupied by a [View] widget, which will start a new rendering zone.
///
/// {@template flutter.widgets.ViewAnchor}
/// An example use case for this widget is a tooltip for a button. The tooltip
/// should be able to extend beyond the bounds of the main view. For this, the
/// tooltip can be implemented as a separate [View], which is anchored to the
/// button in the main view by wrapping that button with a [ViewAnchor]. In this
/// example, the [view] slot is configured with the tooltip [View] and the
/// [child] is the button widget rendered into the surrounding view.
/// {@endtemplate}
class ViewAnchor extends StatelessWidget {
/// Creates a [ViewAnchor] widget.
const ViewAnchor({
super.key,
this.view,
required this.child,
});
/// The widget that defines the view anchored to this widget.
///
/// Typically, a [View] or [ViewCollection] widget is used, which may be
/// wrapped in other non-[RenderObjectWidget]s (e.g. [InheritedWidget]s).
///
/// {@macro flutter.widgets.ViewAnchor}
final Widget? view;
/// The widget below this widget in the tree.
///
/// It is rendered into the surrounding view, not in the view defined by
/// [view].
///
/// {@macro flutter.widgets.ViewAnchor}
final Widget child;
@override
Widget build(BuildContext context) {
return _MultiChildComponentWidget(
views: <Widget>[
if (view != null)
_ViewScope(
view: null,
child: view!,
),
],
child: child,
);
}
}
class _MultiChildComponentElement extends Element {
_MultiChildComponentElement(super.widget);
List<Element> _viewElements = <Element>[];
final Set<Element> _forgottenViewElements = HashSet<Element>();
Element? _childElement;
bool _debugAssertChildren() {
final _MultiChildComponentWidget typedWidget = widget as _MultiChildComponentWidget;
// Each view widget must have a corresponding element.
assert(_viewElements.length == typedWidget._views.length);
// Iff there is a child widget, it must have a corresponding element.
assert((_childElement == null) == (typedWidget._child == null));
// The child element is not also a view element.
assert(!_viewElements.contains(_childElement));
return true;
}
@override
void attachRenderObject(Object? newSlot) {
super.attachRenderObject(newSlot);
assert(_debugCheckMustAttachRenderObject(newSlot));
}
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
assert(_debugCheckMustAttachRenderObject(newSlot));
assert(_viewElements.isEmpty);
assert(_childElement == null);
rebuild();
assert(_debugAssertChildren());
}
@override
void updateSlot(Object? newSlot) {
super.updateSlot(newSlot);
assert(_debugCheckMustAttachRenderObject(newSlot));
}
bool _debugCheckMustAttachRenderObject(Object? slot) {
// Check only applies in the ViewCollection configuration.
if (!kDebugMode || (widget as _MultiChildComponentWidget)._child != null) {
return true;
}
bool hasAncestorRenderObjectElement = false;
bool ancestorWantsRenderObject = true;
visitAncestorElements((Element ancestor) {
if (!ancestor.debugExpectsRenderObjectForSlot(slot)) {
ancestorWantsRenderObject = false;
return false;
}
if (ancestor is RenderObjectElement) {
hasAncestorRenderObjectElement = true;
return false;
}
return true;
});
if (hasAncestorRenderObjectElement && ancestorWantsRenderObject) {
FlutterError.reportError(
FlutterErrorDetails(exception: FlutterError.fromParts(
<DiagnosticsNode>[
ErrorSummary(
'The Element for ${toStringShort()} cannot be inserted into slot "$slot" of its ancestor. ',
),
ErrorDescription(
'The ownership chain for the Element in question was:\n ${debugGetCreatorChain(10)}',
),
ErrorDescription(
'This Element allows the creation of multiple independent render trees, which cannot '
'be attached to an ancestor in an existing render tree. However, an ancestor RenderObject '
'is expecting that a child will be attached.'
),
ErrorHint(
'Try moving the subtree that contains the ${toStringShort()} widget into the '
'view property of a ViewAnchor widget or to the root of the widget tree, where '
'it is not expected to attach its RenderObject to its ancestor.',
),
],
)),
);
}
return true;
}
@override
void update(_MultiChildComponentWidget newWidget) {
// Cannot switch from ViewAnchor config to ViewCollection config.
assert((newWidget._child == null) == ((widget as _MultiChildComponentWidget)._child == null));
super.update(newWidget);
rebuild(force: true);
assert(_debugAssertChildren());
}
static const Object _viewSlot = Object();
@override
bool debugExpectsRenderObjectForSlot(Object? slot) => slot != _viewSlot;
@override
void performRebuild() {
final _MultiChildComponentWidget typedWidget = widget as _MultiChildComponentWidget;
_childElement = updateChild(_childElement, typedWidget._child, slot);
final List<Widget> views = typedWidget._views;
_viewElements = updateChildren(
_viewElements,
views,
forgottenChildren: _forgottenViewElements,
slots: List<Object>.generate(views.length, (_) => _viewSlot),
);
_forgottenViewElements.clear();
super.performRebuild(); // clears the dirty flag
assert(_debugAssertChildren());
}
@override
void forgetChild(Element child) {
if (child == _childElement) {
_childElement = null;
} else {
assert(_viewElements.contains(child));
assert(!_forgottenViewElements.contains(child));
_forgottenViewElements.add(child);
}
super.forgetChild(child);
}
@override
void visitChildren(ElementVisitor visitor) {
if (_childElement != null) {
visitor(_childElement!);
}
for (final Element child in _viewElements) {
if (!_forgottenViewElements.contains(child)) {
visitor(child);
}
}
}
@override
bool get debugDoingBuild => false; // This element does not have a concept of "building".
@override
Element? get renderObjectAttachingChild => _childElement;
@override
List<DiagnosticsNode> debugDescribeChildren() {
final List<DiagnosticsNode> children = <DiagnosticsNode>[];
if (_childElement != null) {
children.add(_childElement!.toDiagnosticsNode());
}
for (int i = 0; i < _viewElements.length; i++) {
children.add(_viewElements[i].toDiagnosticsNode(
name: 'view ${i + 1}',
style: DiagnosticsTreeStyle.offstage,
));
}
return children;
}
}
// A special [GlobalKey] to support passing the deprecated
// [RendererBinding.renderView] and [RendererBinding.pipelineOwner] to the
// [_RawView]. Will be removed when those deprecated properties are removed.
@optionalTypeArgs
class _DeprecatedRawViewKey<T extends State<StatefulWidget>> extends GlobalKey<T> {
const _DeprecatedRawViewKey(this.view, this.owner, this.renderView) : super.constructor();
final FlutterView view;
final PipelineOwner? owner;
final RenderView? renderView;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is _DeprecatedRawViewKey<T>
&& identical(other.view, view)
&& identical(other.owner, owner)
&& identical(other.renderView, renderView);
}
@override
int get hashCode => Object.hash(view, owner, renderView);
@override
String toString() => '[_DeprecatedRawViewKey ${describeIdentity(view)}]';
}

View File

@ -1103,8 +1103,7 @@ mixin WidgetInspectorService {
renderObject.markNeedsPaint(); renderObject.markNeedsPaint();
renderObject.visitChildren(markTreeNeedsPaint); renderObject.visitChildren(markTreeNeedsPaint);
} }
final RenderObject root = RendererBinding.instance.renderView; RendererBinding.instance.renderViews.forEach(markTreeNeedsPaint);
markTreeNeedsPaint(root);
} else { } else {
debugOnProfilePaint = null; debugOnProfilePaint = null;
} }

View File

@ -18,6 +18,7 @@ export 'package:vector_math/vector_math_64.dart' show Matrix4;
export 'foundation.dart' show UniqueKey; export 'foundation.dart' show UniqueKey;
export 'rendering.dart' show TextSelectionHandleType; export 'rendering.dart' show TextSelectionHandleType;
export 'src/widgets/actions.dart'; export 'src/widgets/actions.dart';
export 'src/widgets/adapter.dart';
export 'src/widgets/animated_cross_fade.dart'; export 'src/widgets/animated_cross_fade.dart';
export 'src/widgets/animated_scroll_view.dart'; export 'src/widgets/animated_scroll_view.dart';
export 'src/widgets/animated_size.dart'; export 'src/widgets/animated_size.dart';

View File

@ -117,9 +117,17 @@ Future<Map<String, dynamic>> hasReassemble(Future<Map<String, dynamic>> pendingR
void main() { void main() {
final List<String?> console = <String?>[]; final List<String?> console = <String?>[];
late PipelineOwner owner;
setUpAll(() async { setUpAll(() async {
binding = TestServiceExtensionsBinding()..scheduleFrame(); binding = TestServiceExtensionsBinding();
final RenderView view = RenderView(view: binding.platformDispatcher.views.single);
owner = PipelineOwner(onSemanticsUpdate: (ui.SemanticsUpdate _) { })
..rootNode = view;
binding.rootPipelineOwner.adoptChild(owner);
binding.addRenderView(view);
view.prepareInitialFrame();
binding.scheduleFrame();
expect(binding.frameScheduled, isTrue); expect(binding.frameScheduled, isTrue);
// We need to test this service extension here because the result is true // We need to test this service extension here because the result is true
@ -176,6 +184,10 @@ void main() {
expect(console, isEmpty); expect(console, isEmpty);
debugPrint = debugPrintThrottled; debugPrint = debugPrintThrottled;
binding.rootPipelineOwner.dropChild(owner);
owner
..rootNode = null
..dispose();
}); });
// The following list is alphabetical, one test per extension. // The following list is alphabetical, one test per extension.
@ -268,11 +280,13 @@ void main() {
await binding.doFrame(); await binding.doFrame();
final Map<String, dynamic> result = await binding.testExtension(RenderingServiceExtensions.debugDumpSemanticsTreeInTraversalOrder.name, <String, String>{}); final Map<String, dynamic> result = await binding.testExtension(RenderingServiceExtensions.debugDumpSemanticsTreeInTraversalOrder.name, <String, String>{});
expect(result, <String, String>{ expect(result, <String, Object>{
'data': 'Semantics not generated.\n' 'data': matches(
'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n' r'Semantics not generated for RenderView#[0-9a-f]{5}\.\n'
'Usually, platforms only ask for semantics when assistive technologies (like screen readers) are running.\n' r'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n'
'To generate semantics, try turning on an assistive technology (like VoiceOver or TalkBack) on your device.' r'Usually, platforms only ask for semantics when assistive technologies \(like screen readers\) are running.\n'
r'To generate semantics, try turning on an assistive technology \(like VoiceOver or TalkBack\) on your device.'
)
}); });
}); });
@ -280,11 +294,13 @@ void main() {
await binding.doFrame(); await binding.doFrame();
final Map<String, dynamic> result = await binding.testExtension(RenderingServiceExtensions.debugDumpSemanticsTreeInInverseHitTestOrder.name, <String, String>{}); final Map<String, dynamic> result = await binding.testExtension(RenderingServiceExtensions.debugDumpSemanticsTreeInInverseHitTestOrder.name, <String, String>{});
expect(result, <String, String>{ expect(result, <String, Object>{
'data': 'Semantics not generated.\n' 'data': matches(
'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n' r'Semantics not generated for RenderView#[0-9a-f]{5}\.\n'
'Usually, platforms only ask for semantics when assistive technologies (like screen readers) are running.\n' r'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n'
'To generate semantics, try turning on an assistive technology (like VoiceOver or TalkBack) on your device.' r'Usually, platforms only ask for semantics when assistive technologies \(like screen readers\) are running.\n'
r'To generate semantics, try turning on an assistive technology \(like VoiceOver or TalkBack\) on your device.'
)
}); });
}); });

View File

@ -13,20 +13,20 @@ void main() {
tearDown(() { tearDown(() {
final List<PipelineOwner> children = <PipelineOwner>[]; final List<PipelineOwner> children = <PipelineOwner>[];
RendererBinding.instance.pipelineOwner.visitChildren((PipelineOwner child) { RendererBinding.instance.rootPipelineOwner.visitChildren((PipelineOwner child) {
children.add(child); children.add(child);
}); });
children.forEach(RendererBinding.instance.pipelineOwner.dropChild); children.forEach(RendererBinding.instance.rootPipelineOwner.dropChild);
}); });
test("BindingPipelineManifold notifies binding if render object managed by binding's PipelineOwner tree needs visual update", () { test("BindingPipelineManifold notifies binding if render object managed by binding's PipelineOwner tree needs visual update", () {
final PipelineOwner child = PipelineOwner(); final PipelineOwner child = PipelineOwner();
RendererBinding.instance.pipelineOwner.adoptChild(child); RendererBinding.instance.rootPipelineOwner.adoptChild(child);
final RenderObject renderObject = TestRenderObject(); final RenderObject renderObject = TestRenderObject();
child.rootNode = renderObject; child.rootNode = renderObject;
renderObject.scheduleInitialLayout(); renderObject.scheduleInitialLayout();
RendererBinding.instance.pipelineOwner.flushLayout(); RendererBinding.instance.rootPipelineOwner.flushLayout();
MyTestRenderingFlutterBinding.instance.ensureVisualUpdateCount = 0; MyTestRenderingFlutterBinding.instance.ensureVisualUpdateCount = 0;
renderObject.markNeedsLayout(); renderObject.markNeedsLayout();
@ -37,20 +37,20 @@ void main() {
final PipelineOwner child = PipelineOwner( final PipelineOwner child = PipelineOwner(
onSemanticsUpdate: (_) { }, onSemanticsUpdate: (_) { },
); );
RendererBinding.instance.pipelineOwner.adoptChild(child); RendererBinding.instance.rootPipelineOwner.adoptChild(child);
expect(child.semanticsOwner, isNull); expect(child.semanticsOwner, isNull);
expect(RendererBinding.instance.pipelineOwner.semanticsOwner, isNull); expect(RendererBinding.instance.rootPipelineOwner.semanticsOwner, isNull);
final SemanticsHandle handle = SemanticsBinding.instance.ensureSemantics(); final SemanticsHandle handle = SemanticsBinding.instance.ensureSemantics();
expect(child.semanticsOwner, isNotNull); expect(child.semanticsOwner, isNotNull);
expect(RendererBinding.instance.pipelineOwner.semanticsOwner, isNotNull); expect(RendererBinding.instance.rootPipelineOwner.semanticsOwner, isNotNull);
handle.dispose(); handle.dispose();
expect(child.semanticsOwner, isNull); expect(child.semanticsOwner, isNull);
expect(RendererBinding.instance.pipelineOwner.semanticsOwner, isNull); expect(RendererBinding.instance.rootPipelineOwner.semanticsOwner, isNull);
}); });
} }

View File

@ -10,29 +10,48 @@ import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
test('handleMetricsChanged does not scheduleForcedFrame unless there is a child to the renderView', () async { test('handleMetricsChanged does not scheduleForcedFrame unless there a registered renderView with a child', () async {
expect(SchedulerBinding.instance.hasScheduledFrame, false); expect(SchedulerBinding.instance.hasScheduledFrame, false);
RendererBinding.instance.handleMetricsChanged(); RendererBinding.instance.handleMetricsChanged();
expect(SchedulerBinding.instance.hasScheduledFrame, false); expect(SchedulerBinding.instance.hasScheduledFrame, false);
RendererBinding.instance.addRenderView(RendererBinding.instance.renderView);
RendererBinding.instance.handleMetricsChanged();
expect(SchedulerBinding.instance.hasScheduledFrame, false);
RendererBinding.instance.renderView.child = RenderLimitedBox(); RendererBinding.instance.renderView.child = RenderLimitedBox();
RendererBinding.instance.handleMetricsChanged(); RendererBinding.instance.handleMetricsChanged();
expect(SchedulerBinding.instance.hasScheduledFrame, true); expect(SchedulerBinding.instance.hasScheduledFrame, true);
RendererBinding.instance.removeRenderView(RendererBinding.instance.renderView);
}); });
test('debugDumpSemantics prints explanation when semantics are unavailable', () { test('debugDumpSemantics prints explanation when semantics are unavailable', () {
RendererBinding.instance.addRenderView(RendererBinding.instance.renderView);
final List<String?> log = <String?>[]; final List<String?> log = <String?>[];
debugPrint = (String? message, {int? wrapWidth}) { debugPrint = (String? message, {int? wrapWidth}) {
log.add(message); log.add(message);
}; };
debugDumpSemanticsTree(); debugDumpSemanticsTree();
expect(log, hasLength(1)); expect(log, hasLength(1));
expect( expect(log.single, startsWith('Semantics not generated'));
log.single, expect(log.single, endsWith(
'Semantics not generated.\n'
'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n' 'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n'
'Usually, platforms only ask for semantics when assistive technologies (like screen readers) are running.\n' 'Usually, platforms only ask for semantics when assistive technologies (like screen readers) are running.\n'
'To generate semantics, try turning on an assistive technology (like VoiceOver or TalkBack) on your device.' 'To generate semantics, try turning on an assistive technology (like VoiceOver or TalkBack) on your device.'
));
RendererBinding.instance.removeRenderView(RendererBinding.instance.renderView);
});
test('root pipeline owner cannot manage root node', () {
final RenderObject rootNode = RenderProxyBox();
expect(
() => RendererBinding.instance.rootPipelineOwner.rootNode = rootNode,
throwsA(isFlutterError.having(
(FlutterError e) => e.message,
'message',
contains('Cannot set a rootNode on the default root pipeline owner.'),
)),
); );
}); });
} }

View File

@ -32,8 +32,21 @@ class TestMouseTrackerFlutterBinding extends BindingBase
postFrameCallbacks = <void Function(Duration)>[]; postFrameCallbacks = <void Function(Duration)>[];
} }
late final RenderView _renderView = RenderView(
view: platformDispatcher.implicitView!,
);
late final PipelineOwner _pipelineOwner = PipelineOwner(
onSemanticsUpdate: (ui.SemanticsUpdate _) { assert(false); },
);
void setHitTest(BoxHitTest hitTest) { void setHitTest(BoxHitTest hitTest) {
renderView.child = _TestHitTester(hitTest); if (_pipelineOwner.rootNode == null) {
_pipelineOwner.rootNode = _renderView;
rootPipelineOwner.adoptChild(_pipelineOwner);
addRenderView(_renderView);
}
_renderView.child = _TestHitTester(hitTest);
} }
SchedulerPhase? _overridePhase; SchedulerPhase? _overridePhase;

View File

@ -0,0 +1,208 @@
// 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';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
final RendererBinding binding = RenderingFlutterBinding.ensureInitialized();
test('Adding/removing renderviews updates renderViews getter', () {
final FlutterView flutterView = FakeFlutterView();
final RenderView view = RenderView(view: flutterView);
expect(binding.renderViews, isEmpty);
binding.addRenderView(view);
expect(binding.renderViews, contains(view));
expect(view.configuration.devicePixelRatio, flutterView.devicePixelRatio);
expect(view.configuration.size, flutterView.physicalSize / flutterView.devicePixelRatio);
binding.removeRenderView(view);
expect(binding.renderViews, isEmpty);
});
test('illegal add/remove renderviews', () {
final FlutterView flutterView = FakeFlutterView();
final RenderView view1 = RenderView(view: flutterView);
final RenderView view2 = RenderView(view: flutterView);
final RenderView view3 = RenderView(view: FakeFlutterView(viewId: 200));
expect(binding.renderViews, isEmpty);
binding.addRenderView(view1);
expect(binding.renderViews, contains(view1));
expect(() => binding.addRenderView(view1), throwsAssertionError);
expect(() => binding.addRenderView(view2), throwsAssertionError);
expect(() => binding.removeRenderView(view2), throwsAssertionError);
expect(() => binding.removeRenderView(view3), throwsAssertionError);
expect(binding.renderViews, contains(view1));
binding.removeRenderView(view1);
expect(binding.renderViews, isEmpty);
expect(() => binding.removeRenderView(view1), throwsAssertionError);
expect(() => binding.removeRenderView(view2), throwsAssertionError);
});
test('changing metrics updates configuration', () {
final FakeFlutterView flutterView = FakeFlutterView();
final RenderView view = RenderView(view: flutterView);
binding.addRenderView(view);
expect(view.configuration.devicePixelRatio, 2.5);
expect(view.configuration.size, const Size(160.0, 240.0));
flutterView.devicePixelRatio = 3.0;
flutterView.physicalSize = const Size(300, 300);
binding.handleMetricsChanged();
expect(view.configuration.devicePixelRatio, 3.0);
expect(view.configuration.size, const Size(100.0, 100.0));
binding.removeRenderView(view);
});
test('semantics actions are performed on the right view', () {
final FakeFlutterView flutterView1 = FakeFlutterView(viewId: 1);
final FakeFlutterView flutterView2 = FakeFlutterView(viewId: 2);
final RenderView renderView1 = RenderView(view: flutterView1);
final RenderView renderView2 = RenderView(view: flutterView2);
final PipelineOwnerSpy owner1 = PipelineOwnerSpy()
..rootNode = renderView1;
final PipelineOwnerSpy owner2 = PipelineOwnerSpy()
..rootNode = renderView2;
binding.addRenderView(renderView1);
binding.addRenderView(renderView2);
binding.performSemanticsAction(
const SemanticsActionEvent(type: SemanticsAction.copy, viewId: 1, nodeId: 11),
);
expect(owner1.semanticsOwner.performedActions.single, (11, SemanticsAction.copy, null));
expect(owner2.semanticsOwner.performedActions, isEmpty);
owner1.semanticsOwner.performedActions.clear();
binding.performSemanticsAction(
const SemanticsActionEvent(type: SemanticsAction.tap, viewId: 2, nodeId: 22),
);
expect(owner1.semanticsOwner.performedActions, isEmpty);
expect(owner2.semanticsOwner.performedActions.single, (22, SemanticsAction.tap, null));
owner2.semanticsOwner.performedActions.clear();
binding.performSemanticsAction(
const SemanticsActionEvent(type: SemanticsAction.tap, viewId: 3, nodeId: 22),
);
expect(owner1.semanticsOwner.performedActions, isEmpty);
expect(owner2.semanticsOwner.performedActions, isEmpty);
binding.removeRenderView(renderView1);
binding.removeRenderView(renderView2);
});
test('all registered renderviews are asked to composite frame', () {
final FakeFlutterView flutterView1 = FakeFlutterView(viewId: 1);
final FakeFlutterView flutterView2 = FakeFlutterView(viewId: 2);
final RenderView renderView1 = RenderView(view: flutterView1);
final RenderView renderView2 = RenderView(view: flutterView2);
final PipelineOwner owner1 = PipelineOwner()..rootNode = renderView1;
final PipelineOwner owner2 = PipelineOwner()..rootNode = renderView2;
binding.rootPipelineOwner.adoptChild(owner1);
binding.rootPipelineOwner.adoptChild(owner2);
binding.addRenderView(renderView1);
binding.addRenderView(renderView2);
renderView1.prepareInitialFrame();
renderView2.prepareInitialFrame();
expect(flutterView1.renderedScenes, isEmpty);
expect(flutterView2.renderedScenes, isEmpty);
binding.handleBeginFrame(Duration.zero);
binding.handleDrawFrame();
expect(flutterView1.renderedScenes, hasLength(1));
expect(flutterView2.renderedScenes, hasLength(1));
binding.removeRenderView(renderView1);
binding.handleBeginFrame(Duration.zero);
binding.handleDrawFrame();
expect(flutterView1.renderedScenes, hasLength(1));
expect(flutterView2.renderedScenes, hasLength(2));
binding.removeRenderView(renderView2);
binding.handleBeginFrame(Duration.zero);
binding.handleDrawFrame();
expect(flutterView1.renderedScenes, hasLength(1));
expect(flutterView2.renderedScenes, hasLength(2));
});
test('hit-testing reaches the right view', () {
final FakeFlutterView flutterView1 = FakeFlutterView(viewId: 1);
final FakeFlutterView flutterView2 = FakeFlutterView(viewId: 2);
final RenderView renderView1 = RenderView(view: flutterView1);
final RenderView renderView2 = RenderView(view: flutterView2);
binding.addRenderView(renderView1);
binding.addRenderView(renderView2);
HitTestResult result = HitTestResult();
binding.hitTestInView(result, Offset.zero, 1);
expect(result.path, hasLength(2));
expect(result.path.first.target, renderView1);
expect(result.path.last.target, binding);
result = HitTestResult();
binding.hitTestInView(result, Offset.zero, 2);
expect(result.path, hasLength(2));
expect(result.path.first.target, renderView2);
expect(result.path.last.target, binding);
result = HitTestResult();
binding.hitTestInView(result, Offset.zero, 3);
expect(result.path.single.target, binding);
binding.removeRenderView(renderView1);
binding.removeRenderView(renderView2);
});
}
class FakeFlutterView extends Fake implements FlutterView {
FakeFlutterView({
this.viewId = 100,
this.devicePixelRatio = 2.5,
this.physicalSize = const Size(400,600),
this.padding = FakeViewPadding.zero,
});
@override
final int viewId;
@override
double devicePixelRatio;
@override
Size physicalSize;
@override
ViewPadding padding;
List<Scene> renderedScenes = <Scene>[];
@override
void render(Scene scene) {
renderedScenes.add(scene);
}
}
class PipelineOwnerSpy extends PipelineOwner {
@override
final SemanticsOwnerSpy semanticsOwner = SemanticsOwnerSpy();
}
class SemanticsOwnerSpy extends Fake implements SemanticsOwner {
final List<(int, SemanticsAction, Object?)> performedActions = <(int, SemanticsAction, Object?)>[];
@override
void performAction(int id, SemanticsAction action, [ Object? args ]) {
performedActions.add((id, action, args));
}
}

View File

@ -678,20 +678,43 @@ void main() {
expect(root.semanticsOwner, isNotNull); expect(root.semanticsOwner, isNotNull);
expect(child.semanticsOwner, isNotNull); expect(child.semanticsOwner, isNotNull);
expect(childOfChild.semanticsOwner, isNull); expect(childOfChild.semanticsOwner, isNotNull); // Retained in case we get re-attached.
final SemanticsHandle childSemantics = child.ensureSemantics(); final SemanticsHandle childSemantics = child.ensureSemantics();
root.dropChild(child); root.dropChild(child);
expect(root.semanticsOwner, isNotNull); expect(root.semanticsOwner, isNotNull);
expect(child.semanticsOwner, isNotNull); expect(child.semanticsOwner, isNotNull);
expect(childOfChild.semanticsOwner, isNull); expect(childOfChild.semanticsOwner, isNotNull); // Retained in case we get re-attached.
childSemantics.dispose(); childSemantics.dispose();
expect(root.semanticsOwner, isNotNull); expect(root.semanticsOwner, isNotNull);
expect(child.semanticsOwner, isNull); expect(child.semanticsOwner, isNull);
expect(childOfChild.semanticsOwner, isNull); expect(childOfChild.semanticsOwner, isNotNull);
manifold.semanticsEnabled = false;
expect(root.semanticsOwner, isNull);
expect(childOfChild.semanticsOwner, isNotNull);
root.adoptChild(childOfChild);
expect(root.semanticsOwner, isNull);
expect(childOfChild.semanticsOwner, isNull); // Disposed on re-attachment.
manifold.semanticsEnabled = true;
expect(root.semanticsOwner, isNotNull);
expect(childOfChild.semanticsOwner, isNotNull);
root.dropChild(childOfChild);
expect(root.semanticsOwner, isNotNull);
expect(childOfChild.semanticsOwner, isNotNull);
childOfChild.dispose();
expect(root.semanticsOwner, isNotNull);
expect(childOfChild.semanticsOwner, isNull); // Disposed on dispose.
}); });
test('can adopt/drop children during own layout', () { test('can adopt/drop children during own layout', () {
@ -789,6 +812,38 @@ void main() {
}); });
expect(children.single, childOfChild3); expect(children.single, childOfChild3);
}); });
test('printing pipeline owner tree smoke test', () {
final PipelineOwner root = PipelineOwner();
final PipelineOwner child1 = PipelineOwner()
..rootNode = FakeRenderView();
final PipelineOwner childOfChild1 = PipelineOwner()
..rootNode = FakeRenderView();
final PipelineOwner child2 = PipelineOwner()
..rootNode = FakeRenderView();
final PipelineOwner childOfChild2 = PipelineOwner()
..rootNode = FakeRenderView();
root.adoptChild(child1);
child1.adoptChild(childOfChild1);
root.adoptChild(child2);
child2.adoptChild(childOfChild2);
expect(root.toStringDeep(), equalsIgnoringHashCodes(
'PipelineOwner#00000\n'
' ├─PipelineOwner#00000\n'
' │ │ rootNode: FakeRenderView#00000 NEEDS-LAYOUT NEEDS-PAINT\n'
' │ │\n'
' │ └─PipelineOwner#00000\n'
' │ rootNode: FakeRenderView#00000 NEEDS-LAYOUT NEEDS-PAINT\n'
'\n'
' └─PipelineOwner#00000\n'
' │ rootNode: FakeRenderView#00000 NEEDS-LAYOUT NEEDS-PAINT\n'
'\n'
' └─PipelineOwner#00000\n'
' rootNode: FakeRenderView#00000 NEEDS-LAYOUT NEEDS-PAINT\n'
));
});
} }
class TestPipelineManifold extends ChangeNotifier implements PipelineManifold { class TestPipelineManifold extends ChangeNotifier implements PipelineManifold {
@ -860,3 +915,5 @@ List<PipelineOwner> _treeWalk(PipelineOwner root) {
root.visitChildren(visitor); root.visitChildren(visitor);
return results; return results;
} }
class FakeRenderView extends RenderBox { }

View File

@ -47,7 +47,7 @@ void main() {
child: platformViewRenderBox, child: platformViewRenderBox,
); );
int semanticsUpdateCount = 0; int semanticsUpdateCount = 0;
final SemanticsHandle semanticsHandle = TestRenderingFlutterBinding.instance.pipelineOwner.ensureSemantics( final SemanticsHandle semanticsHandle = TestRenderingFlutterBinding.instance.rootPipelineOwner.ensureSemantics(
listener: () { listener: () {
++semanticsUpdateCount; ++semanticsUpdateCount;
}, },

View File

@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:ui' show SemanticsUpdate;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
@ -43,6 +44,44 @@ class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, Ser
void initInstances() { void initInstances() {
super.initInstances(); super.initInstances();
_instance = this; _instance = this;
// TODO(goderbauer): Create (fake) window if embedder doesn't provide an implicit view.
assert(platformDispatcher.implicitView != null);
_renderView = initRenderView(platformDispatcher.implicitView!);
}
@override
RenderView get renderView => _renderView;
late RenderView _renderView;
@override
PipelineOwner get pipelineOwner => rootPipelineOwner;
/// Creates a [RenderView] object to be the root of the
/// [RenderObject] rendering tree, and initializes it so that it
/// will be rendered when the next frame is requested.
///
/// Called automatically when the binding is created.
RenderView initRenderView(FlutterView view) {
final RenderView renderView = RenderView(view: view);
rootPipelineOwner.rootNode = renderView;
addRenderView(renderView);
renderView.prepareInitialFrame();
return renderView;
}
@override
PipelineOwner createRootPipelineOwner() {
return PipelineOwner(
onSemanticsOwnerCreated: () {
renderView.scheduleInitialSemantics();
},
onSemanticsUpdate: (SemanticsUpdate update) {
renderView.updateSemantics(update);
},
onSemanticsOwnerDisposed: () {
renderView.clearSemantics();
},
);
} }
/// Creates and initializes the binding. This function is /// Creates and initializes the binding. This function is
@ -139,23 +178,25 @@ class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, Ser
final FlutterExceptionHandler? oldErrorHandler = FlutterError.onError; final FlutterExceptionHandler? oldErrorHandler = FlutterError.onError;
FlutterError.onError = _errors.add; FlutterError.onError = _errors.add;
try { try {
pipelineOwner.flushLayout(); rootPipelineOwner.flushLayout();
if (phase == EnginePhase.layout) { if (phase == EnginePhase.layout) {
return; return;
} }
pipelineOwner.flushCompositingBits(); rootPipelineOwner.flushCompositingBits();
if (phase == EnginePhase.compositingBits) { if (phase == EnginePhase.compositingBits) {
return; return;
} }
pipelineOwner.flushPaint(); rootPipelineOwner.flushPaint();
if (phase == EnginePhase.paint) { if (phase == EnginePhase.paint) {
return; return;
} }
for (final RenderView renderView in renderViews) {
renderView.compositeFrame(); renderView.compositeFrame();
}
if (phase == EnginePhase.composite) { if (phase == EnginePhase.composite) {
return; return;
} }
pipelineOwner.flushSemantics(); rootPipelineOwner.flushSemantics();
if (phase == EnginePhase.flushSemantics) { if (phase == EnginePhase.flushSemantics) {
return; return;
} }

View File

@ -122,6 +122,16 @@ void main() {
isNot(paintsGreenRect), isNot(paintsGreenRect),
); );
}); });
test('Config can be set and changed after instantiation without calling prepareInitialFrame first', () {
final RenderView view = RenderView(
view: RendererBinding.instance.platformDispatcher.views.single,
);
view.configuration = const ViewConfiguration(size: Size(100, 200), devicePixelRatio: 3.0);
view.configuration = const ViewConfiguration(size: Size(200, 300), devicePixelRatio: 2.0);
PipelineOwner().rootNode = view;
view.prepareInitialFrame();
});
} }
const Color orange = Color(0xFFFF9000); const Color orange = Color(0xFFFF9000);

View File

@ -42,7 +42,7 @@ void main() {
await benchmarkWidgets( await benchmarkWidgets(
(WidgetTester tester) async { (WidgetTester tester) async {
const Key root = Key('root'); const Key root = Key('root');
binding.attachRootWidget(Container(key: root)); binding.attachRootWidget(binding.wrapWithDefaultView(Container(key: root)));
await tester.pump(); await tester.pump();
expect(binding.framesBegun, greaterThan(0)); expect(binding.framesBegun, greaterThan(0));

View File

@ -156,8 +156,9 @@ void main() {
equalsIgnoringHashCodes( equalsIgnoringHashCodes(
'RenderPadding#00000 relayoutBoundary=up1\n' 'RenderPadding#00000 relayoutBoundary=up1\n'
' │ creator: Padding ← Container ← Align ← MediaQuery ←\n' ' │ creator: Padding ← Container ← Align ← MediaQuery ←\n'
' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n' ' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n'
' │ TestFlutterView#00000] ← [root]\n' ' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
' │ [root]\n'
' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n' ' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n' ' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n'
' │ size: Size(63.0, 88.0)\n' ' │ size: Size(63.0, 88.0)\n'
@ -165,8 +166,9 @@ void main() {
'\n' '\n'
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up2\n' ' └─child: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
' │ creator: ConstrainedBox ← Padding ← Container ← Align ←\n' ' │ creator: ConstrainedBox ← Padding ← Container ← Align ←\n'
' │ MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n' ' │ MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope ←\n'
' │ View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n' ' │ _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
' │ TestFlutterView#00000] ← View ← [root]\n'
' │ parentData: offset=Offset(5.0, 5.0) (can use size)\n' ' │ parentData: offset=Offset(5.0, 5.0) (can use size)\n'
' │ constraints: BoxConstraints(0.0<=w<=790.0, 0.0<=h<=590.0)\n' ' │ constraints: BoxConstraints(0.0<=w<=790.0, 0.0<=h<=590.0)\n'
' │ size: Size(53.0, 78.0)\n' ' │ size: Size(53.0, 78.0)\n'
@ -174,8 +176,9 @@ void main() {
'\n' '\n'
' └─child: RenderDecoratedBox#00000\n' ' └─child: RenderDecoratedBox#00000\n'
' │ creator: DecoratedBox ← ConstrainedBox ← Padding ← Container ←\n' ' │ creator: DecoratedBox ← ConstrainedBox ← Padding ← Container ←\n'
' │ Align ← MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n' ' │ Align ← MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope\n'
' │ View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n' ' │ ← _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
' │ TestFlutterView#00000] ← View ← [root]\n'
' │ parentData: <none> (can use size)\n' ' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
' │ size: Size(53.0, 78.0)\n' ' │ size: Size(53.0, 78.0)\n'
@ -188,8 +191,9 @@ void main() {
' └─child: _RenderColoredBox#00000\n' ' └─child: _RenderColoredBox#00000\n'
' │ creator: ColoredBox ← DecoratedBox ← ConstrainedBox ← Padding ←\n' ' │ creator: ColoredBox ← DecoratedBox ← ConstrainedBox ← Padding ←\n'
' │ Container ← Align ← MediaQuery ← _MediaQueryFromView ←\n' ' │ Container ← Align ← MediaQuery ← _MediaQueryFromView ←\n'
' │ _ViewScope ← View-[GlobalObjectKey TestFlutterView#00000] ←\n' ' │ _PipelineOwnerScope ← _ViewScope ←\n'
' │ [root]\n' ' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
' │ ⋯\n'
' │ parentData: <none> (can use size)\n' ' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
' │ size: Size(53.0, 78.0)\n' ' │ size: Size(53.0, 78.0)\n'
@ -198,8 +202,8 @@ void main() {
' └─child: RenderPadding#00000\n' ' └─child: RenderPadding#00000\n'
' │ creator: Padding ← ColoredBox ← DecoratedBox ← ConstrainedBox ←\n' ' │ creator: Padding ← ColoredBox ← DecoratedBox ← ConstrainedBox ←\n'
' │ Padding ← Container ← Align ← MediaQuery ← _MediaQueryFromView\n' ' │ Padding ← Container ← Align ← MediaQuery ← _MediaQueryFromView\n'
' │ ← _ViewScope ← View-[GlobalObjectKey TestFlutterView#00000]\n' ' │ ← _PipelineOwnerScope ← _ViewScope\n'
'[root]\n' '_RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← ⋯\n'
' │ parentData: <none> (can use size)\n' ' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
' │ size: Size(53.0, 78.0)\n' ' │ size: Size(53.0, 78.0)\n'
@ -208,8 +212,7 @@ void main() {
' └─child: RenderPositionedBox#00000\n' ' └─child: RenderPositionedBox#00000\n'
' │ creator: Align ← Padding ← ColoredBox ← DecoratedBox ←\n' ' │ creator: Align ← Padding ← ColoredBox ← DecoratedBox ←\n'
' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n' ' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n'
' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n' ' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ← ⋯\n'
' │ TestFlutterView#00000] ← ⋯\n'
' │ parentData: offset=Offset(7.0, 7.0) (can use size)\n' ' │ parentData: offset=Offset(7.0, 7.0) (can use size)\n'
' │ constraints: BoxConstraints(w=39.0, h=64.0)\n' ' │ constraints: BoxConstraints(w=39.0, h=64.0)\n'
' │ size: Size(39.0, 64.0)\n' ' │ size: Size(39.0, 64.0)\n'
@ -220,7 +223,7 @@ void main() {
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up1\n' ' └─child: RenderConstrainedBox#00000 relayoutBoundary=up1\n'
' │ creator: SizedBox ← Align ← Padding ← ColoredBox ← DecoratedBox ←\n' ' │ creator: SizedBox ← Align ← Padding ← ColoredBox ← DecoratedBox ←\n'
' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n' ' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n'
' │ _MediaQueryFromView ← _ViewScope ← ⋯\n' ' │ _MediaQueryFromView ← _PipelineOwnerScope ← ⋯\n'
' │ parentData: offset=Offset(14.0, 31.0) (can use size)\n' ' │ parentData: offset=Offset(14.0, 31.0) (can use size)\n'
' │ constraints: BoxConstraints(0.0<=w<=39.0, 0.0<=h<=64.0)\n' ' │ constraints: BoxConstraints(0.0<=w<=39.0, 0.0<=h<=64.0)\n'
' │ size: Size(25.0, 33.0)\n' ' │ size: Size(25.0, 33.0)\n'
@ -255,8 +258,9 @@ void main() {
equalsIgnoringHashCodes( equalsIgnoringHashCodes(
'RenderPadding#00000 relayoutBoundary=up1\n' 'RenderPadding#00000 relayoutBoundary=up1\n'
' │ creator: Padding ← Container ← Align ← MediaQuery ←\n' ' │ creator: Padding ← Container ← Align ← MediaQuery ←\n'
' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n' ' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n'
' │ TestFlutterView#00000] ← [root]\n' ' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
' │ [root]\n'
' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n' ' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n' ' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n'
' │ layer: null\n' ' │ layer: null\n'
@ -267,8 +271,9 @@ void main() {
'\n' '\n'
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up2\n' ' └─child: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
' │ creator: ConstrainedBox ← Padding ← Container ← Align ←\n' ' │ creator: ConstrainedBox ← Padding ← Container ← Align ←\n'
' │ MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n' ' │ MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope ←\n'
' │ View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n' ' │ _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
' │ TestFlutterView#00000] ← View ← [root]\n'
' │ parentData: offset=Offset(5.0, 5.0) (can use size)\n' ' │ parentData: offset=Offset(5.0, 5.0) (can use size)\n'
' │ constraints: BoxConstraints(0.0<=w<=790.0, 0.0<=h<=590.0)\n' ' │ constraints: BoxConstraints(0.0<=w<=790.0, 0.0<=h<=590.0)\n'
' │ layer: null\n' ' │ layer: null\n'
@ -278,8 +283,9 @@ void main() {
'\n' '\n'
' └─child: RenderDecoratedBox#00000\n' ' └─child: RenderDecoratedBox#00000\n'
' │ creator: DecoratedBox ← ConstrainedBox ← Padding ← Container ←\n' ' │ creator: DecoratedBox ← ConstrainedBox ← Padding ← Container ←\n'
' │ Align ← MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n' ' │ Align ← MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope\n'
' │ View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n' ' │ ← _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
' │ TestFlutterView#00000] ← View ← [root]\n'
' │ parentData: <none> (can use size)\n' ' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
' │ layer: null\n' ' │ layer: null\n'
@ -300,8 +306,9 @@ void main() {
' └─child: _RenderColoredBox#00000\n' ' └─child: _RenderColoredBox#00000\n'
' │ creator: ColoredBox ← DecoratedBox ← ConstrainedBox ← Padding ←\n' ' │ creator: ColoredBox ← DecoratedBox ← ConstrainedBox ← Padding ←\n'
' │ Container ← Align ← MediaQuery ← _MediaQueryFromView ←\n' ' │ Container ← Align ← MediaQuery ← _MediaQueryFromView ←\n'
' │ _ViewScope ← View-[GlobalObjectKey TestFlutterView#00000] ←\n' ' │ _PipelineOwnerScope ← _ViewScope ←\n'
' │ [root]\n' ' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
' │ ⋯\n'
' │ parentData: <none> (can use size)\n' ' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
' │ layer: null\n' ' │ layer: null\n'
@ -312,8 +319,8 @@ void main() {
' └─child: RenderPadding#00000\n' ' └─child: RenderPadding#00000\n'
' │ creator: Padding ← ColoredBox ← DecoratedBox ← ConstrainedBox ←\n' ' │ creator: Padding ← ColoredBox ← DecoratedBox ← ConstrainedBox ←\n'
' │ Padding ← Container ← Align ← MediaQuery ← _MediaQueryFromView\n' ' │ Padding ← Container ← Align ← MediaQuery ← _MediaQueryFromView\n'
' │ ← _ViewScope ← View-[GlobalObjectKey TestFlutterView#00000]\n' ' │ ← _PipelineOwnerScope ← _ViewScope\n'
'[root]\n' '_RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← ⋯\n'
' │ parentData: <none> (can use size)\n' ' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
' │ layer: null\n' ' │ layer: null\n'
@ -325,8 +332,7 @@ void main() {
' └─child: RenderPositionedBox#00000\n' ' └─child: RenderPositionedBox#00000\n'
' │ creator: Align ← Padding ← ColoredBox ← DecoratedBox ←\n' ' │ creator: Align ← Padding ← ColoredBox ← DecoratedBox ←\n'
' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n' ' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n'
' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n' ' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ← ⋯\n'
' │ TestFlutterView#00000] ← ⋯\n'
' │ parentData: offset=Offset(7.0, 7.0) (can use size)\n' ' │ parentData: offset=Offset(7.0, 7.0) (can use size)\n'
' │ constraints: BoxConstraints(w=39.0, h=64.0)\n' ' │ constraints: BoxConstraints(w=39.0, h=64.0)\n'
' │ layer: null\n' ' │ layer: null\n'
@ -340,7 +346,7 @@ void main() {
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up1\n' ' └─child: RenderConstrainedBox#00000 relayoutBoundary=up1\n'
' │ creator: SizedBox ← Align ← Padding ← ColoredBox ← DecoratedBox ←\n' ' │ creator: SizedBox ← Align ← Padding ← ColoredBox ← DecoratedBox ←\n'
' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n' ' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n'
' │ _MediaQueryFromView ← _ViewScope ← ⋯\n' ' │ _MediaQueryFromView ← _PipelineOwnerScope ← ⋯\n'
' │ parentData: offset=Offset(14.0, 31.0) (can use size)\n' ' │ parentData: offset=Offset(14.0, 31.0) (can use size)\n'
' │ constraints: BoxConstraints(0.0<=w<=39.0, 0.0<=h<=64.0)\n' ' │ constraints: BoxConstraints(0.0<=w<=39.0, 0.0<=h<=64.0)\n'
' │ layer: null\n' ' │ layer: null\n'
@ -367,7 +373,7 @@ void main() {
' shape: rectangle\n' ' shape: rectangle\n'
' configuration: ImageConfiguration(bundle:\n' ' configuration: ImageConfiguration(bundle:\n'
' PlatformAssetBundle#00000(), devicePixelRatio: 3.0, platform:\n' ' PlatformAssetBundle#00000(), devicePixelRatio: 3.0, platform:\n'
' android)\n', ' android)\n'
), ),
); );
}); });
@ -386,8 +392,9 @@ void main() {
'RenderPadding#00000 relayoutBoundary=up1\n' 'RenderPadding#00000 relayoutBoundary=up1\n'
' │ needsCompositing: false\n' ' │ needsCompositing: false\n'
' │ creator: Padding ← Container ← Align ← MediaQuery ←\n' ' │ creator: Padding ← Container ← Align ← MediaQuery ←\n'
' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n' ' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n'
' │ TestFlutterView#00000] ← [root]\n' ' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
' │ [root]\n'
' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n' ' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n' ' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n'
' │ layer: null\n' ' │ layer: null\n'
@ -401,8 +408,9 @@ void main() {
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up2\n' ' └─child: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
' │ needsCompositing: false\n' ' │ needsCompositing: false\n'
' │ creator: ConstrainedBox ← Padding ← Container ← Align ←\n' ' │ creator: ConstrainedBox ← Padding ← Container ← Align ←\n'
' │ MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n' ' │ MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope ←\n'
' │ View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n' ' │ _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
' │ TestFlutterView#00000] ← View ← [root]\n'
' │ parentData: offset=Offset(5.0, 5.0) (can use size)\n' ' │ parentData: offset=Offset(5.0, 5.0) (can use size)\n'
' │ constraints: BoxConstraints(0.0<=w<=790.0, 0.0<=h<=590.0)\n' ' │ constraints: BoxConstraints(0.0<=w<=790.0, 0.0<=h<=590.0)\n'
' │ layer: null\n' ' │ layer: null\n'
@ -415,8 +423,9 @@ void main() {
' └─child: RenderDecoratedBox#00000\n' ' └─child: RenderDecoratedBox#00000\n'
' │ needsCompositing: false\n' ' │ needsCompositing: false\n'
' │ creator: DecoratedBox ← ConstrainedBox ← Padding ← Container ←\n' ' │ creator: DecoratedBox ← ConstrainedBox ← Padding ← Container ←\n'
' │ Align ← MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n' ' │ Align ← MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope\n'
' │ View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n' ' │ ← _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
' │ TestFlutterView#00000] ← View ← [root]\n'
' │ parentData: <none> (can use size)\n' ' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
' │ layer: null\n' ' │ layer: null\n'
@ -440,8 +449,9 @@ void main() {
' │ needsCompositing: false\n' ' │ needsCompositing: false\n'
' │ creator: ColoredBox ← DecoratedBox ← ConstrainedBox ← Padding ←\n' ' │ creator: ColoredBox ← DecoratedBox ← ConstrainedBox ← Padding ←\n'
' │ Container ← Align ← MediaQuery ← _MediaQueryFromView ←\n' ' │ Container ← Align ← MediaQuery ← _MediaQueryFromView ←\n'
' │ _ViewScope ← View-[GlobalObjectKey TestFlutterView#00000] ←\n' ' │ _PipelineOwnerScope ← _ViewScope ←\n'
' │ [root]\n' ' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
' │ ⋯\n'
' │ parentData: <none> (can use size)\n' ' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
' │ layer: null\n' ' │ layer: null\n'
@ -455,8 +465,8 @@ void main() {
' │ needsCompositing: false\n' ' │ needsCompositing: false\n'
' │ creator: Padding ← ColoredBox ← DecoratedBox ← ConstrainedBox ←\n' ' │ creator: Padding ← ColoredBox ← DecoratedBox ← ConstrainedBox ←\n'
' │ Padding ← Container ← Align ← MediaQuery ← _MediaQueryFromView\n' ' │ Padding ← Container ← Align ← MediaQuery ← _MediaQueryFromView\n'
' │ ← _ViewScope ← View-[GlobalObjectKey TestFlutterView#00000]\n' ' │ ← _PipelineOwnerScope ← _ViewScope\n'
'[root]\n' '_RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← ⋯\n'
' │ parentData: <none> (can use size)\n' ' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
' │ layer: null\n' ' │ layer: null\n'
@ -471,8 +481,7 @@ void main() {
' │ needsCompositing: false\n' ' │ needsCompositing: false\n'
' │ creator: Align ← Padding ← ColoredBox ← DecoratedBox ←\n' ' │ creator: Align ← Padding ← ColoredBox ← DecoratedBox ←\n'
' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n' ' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n'
' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n' ' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ← ⋯\n'
' │ TestFlutterView#00000] ← ⋯\n'
' │ parentData: offset=Offset(7.0, 7.0) (can use size)\n' ' │ parentData: offset=Offset(7.0, 7.0) (can use size)\n'
' │ constraints: BoxConstraints(w=39.0, h=64.0)\n' ' │ constraints: BoxConstraints(w=39.0, h=64.0)\n'
' │ layer: null\n' ' │ layer: null\n'
@ -489,7 +498,7 @@ void main() {
' │ needsCompositing: false\n' ' │ needsCompositing: false\n'
' │ creator: SizedBox ← Align ← Padding ← ColoredBox ← DecoratedBox ←\n' ' │ creator: SizedBox ← Align ← Padding ← ColoredBox ← DecoratedBox ←\n'
' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n' ' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n'
' │ _MediaQueryFromView ← _ViewScope ← ⋯\n' ' │ _MediaQueryFromView ← _PipelineOwnerScope ← ⋯\n'
' │ parentData: offset=Offset(14.0, 31.0) (can use size)\n' ' │ parentData: offset=Offset(14.0, 31.0) (can use size)\n'
' │ constraints: BoxConstraints(0.0<=w<=39.0, 0.0<=h<=64.0)\n' ' │ constraints: BoxConstraints(0.0<=w<=39.0, 0.0<=h<=64.0)\n'
' │ layer: null\n' ' │ layer: null\n'
@ -521,7 +530,7 @@ void main() {
' shape: rectangle\n' ' shape: rectangle\n'
' configuration: ImageConfiguration(bundle:\n' ' configuration: ImageConfiguration(bundle:\n'
' PlatformAssetBundle#00000(), devicePixelRatio: 3.0, platform:\n' ' PlatformAssetBundle#00000(), devicePixelRatio: 3.0, platform:\n'
' android)\n', ' android)\n'
), ),
); );
}); });

View File

@ -373,8 +373,9 @@ void main() {
' The following child has no ID: RenderConstrainedBox#00000 NEEDS-LAYOUT NEEDS-PAINT:\n' ' The following child has no ID: RenderConstrainedBox#00000 NEEDS-LAYOUT NEEDS-PAINT:\n'
' creator: ConstrainedBox ← Container ← LayoutWithMissingId ←\n' ' creator: ConstrainedBox ← Container ← LayoutWithMissingId ←\n'
' CustomMultiChildLayout ← Center ← MediaQuery ←\n' ' CustomMultiChildLayout ← Center ← MediaQuery ←\n'
' _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n' ' _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n'
' TestFlutterView#00000] ← [root]\n' ' _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
' [root]\n'
' parentData: offset=Offset(0.0, 0.0); id=null\n' ' parentData: offset=Offset(0.0, 0.0); id=null\n'
' constraints: MISSING\n' ' constraints: MISSING\n'
' size: MISSING\n' ' size: MISSING\n'

View File

@ -144,7 +144,10 @@ void main() {
), ),
); );
} }
return Container(); return View(
view: tester.view,
child: const SizedBox(),
);
}, },
), ),
); );

View File

@ -1227,8 +1227,9 @@ void main() {
'FocusManager#00000\n' 'FocusManager#00000\n'
' │ primaryFocus: FocusNode#00000(Child 4 [PRIMARY FOCUS])\n' ' │ primaryFocus: FocusNode#00000(Child 4 [PRIMARY FOCUS])\n'
' │ primaryFocusCreator: Container-[GlobalKey#00000] ← MediaQuery ←\n' ' │ primaryFocusCreator: Container-[GlobalKey#00000] ← MediaQuery ←\n'
' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n' ' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n'
' │ TestFlutterView#00000] ← [root]\n' ' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
' │ [root]\n'
'\n' '\n'
' └─rootScope: FocusScopeNode#00000(Root Focus Scope [IN FOCUS PATH])\n' ' └─rootScope: FocusScopeNode#00000(Root Focus Scope [IN FOCUS PATH])\n'
' │ IN FOCUS PATH\n' ' │ IN FOCUS PATH\n'

View File

@ -30,7 +30,7 @@ class TestWidgetState extends State<TestWidget> {
void main() { void main() {
testWidgets('initState() is called when we are in the tree', (WidgetTester tester) async { testWidgets('initState() is called when we are in the tree', (WidgetTester tester) async {
await tester.pumpWidget(const Parent(child: TestWidget())); await tester.pumpWidget(const Parent(child: TestWidget()));
expect(ancestors, containsAllInOrder(<String>['Parent', 'View', 'RenderObjectToWidgetAdapter<RenderBox>'])); expect(ancestors, containsAllInOrder(<String>['Parent', 'View', 'RootWidget']));
}); });
} }

View File

@ -205,7 +205,7 @@ void main() {
); );
// The important lines below are the ones marked with "<----" // The important lines below are the ones marked with "<----"
expect(tester.binding.renderView.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes( expect(tester.binding.renderView.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes(
'RenderView#00000\n' '_ReusableRenderView#00000\n'
' │ debug mode enabled - ${Platform.operatingSystem}\n' ' │ debug mode enabled - ${Platform.operatingSystem}\n'
' │ view size: Size(2400.0, 1800.0) (in physical pixels)\n' ' │ view size: Size(2400.0, 1800.0) (in physical pixels)\n'
' │ device pixel ratio: 3.0 (physical pixels per logical pixel)\n' ' │ device pixel ratio: 3.0 (physical pixels per logical pixel)\n'
@ -379,7 +379,7 @@ void main() {
await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0)); await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0));
await tester.pump(); await tester.pump();
expect(tester.binding.renderView.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes( expect(tester.binding.renderView.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes(
'RenderView#00000\n' '_ReusableRenderView#00000\n'
' │ debug mode enabled - ${Platform.operatingSystem}\n' ' │ debug mode enabled - ${Platform.operatingSystem}\n'
' │ view size: Size(2400.0, 1800.0) (in physical pixels)\n' ' │ view size: Size(2400.0, 1800.0) (in physical pixels)\n'
' │ device pixel ratio: 3.0 (physical pixels per logical pixel)\n' ' │ device pixel ratio: 3.0 (physical pixels per logical pixel)\n'

View File

@ -44,26 +44,25 @@ class _MediaQueryAspectVariant extends TestVariant<_MediaQueryAspectCase> {
void main() { void main() {
testWidgets('MediaQuery does not have a default', (WidgetTester tester) async { testWidgets('MediaQuery does not have a default', (WidgetTester tester) async {
bool tested = false; late final FlutterError error;
// Cannot use tester.pumpWidget here because it wraps the widget in a View, // Cannot use tester.pumpWidget here because it wraps the widget in a View,
// which introduces a MediaQuery ancestor. // which introduces a MediaQuery ancestor.
await pumpWidgetWithoutViewWrapper( await pumpWidgetWithoutViewWrapper(
tester: tester, tester: tester,
widget: Builder( widget: Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
tested = true; try {
MediaQuery.of(context); // should throw MediaQuery.of(context);
return Container(); } on FlutterError catch (e) {
error = e;
}
return View(
view: tester.view,
child: const SizedBox(),
);
}, },
), ),
); );
expect(tested, isTrue);
final dynamic exception = tester.takeException();
expect(exception, isNotNull);
expect(exception ,isFlutterError);
final FlutterError error = exception as FlutterError;
expect(error.diagnostics.length, 5);
expect(error.diagnostics.last, isA<ErrorHint>());
expect( expect(
error.toStringDeep(), error.toStringDeep(),
startsWith( startsWith(
@ -119,7 +118,10 @@ void main() {
final MediaQueryData? data = MediaQuery.maybeOf(context); final MediaQueryData? data = MediaQuery.maybeOf(context);
expect(data, isNull); expect(data, isNull);
tested = true; tested = true;
return Container(); return View(
view: tester.view,
child: const SizedBox(),
);
}, },
), ),
); );
@ -295,7 +297,10 @@ void main() {
child: Builder( child: Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
data = MediaQuery.of(context); data = MediaQuery.of(context);
return const Placeholder(); return View(
view: tester.view,
child: const SizedBox(),
);
}, },
) )
); );
@ -348,7 +353,10 @@ void main() {
builder: (BuildContext context) { builder: (BuildContext context) {
rebuildCount++; rebuildCount++;
data = MediaQuery.of(context); data = MediaQuery.of(context);
return const Placeholder(); return View(
view: tester.view,
child: const SizedBox(),
);
}, },
), ),
); );

View File

@ -0,0 +1,39 @@
// 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 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('runApp uses deprecated pipelineOwner and renderView', (WidgetTester tester) async {
runApp(const SizedBox());
final RenderObject renderObject = tester.renderObject(find.byType(SizedBox));
RenderObject parent = renderObject;
while (parent.parent != null) {
parent = parent.parent!;
}
expect(parent, isA<RenderView>());
expect(parent, equals(tester.binding.renderView));
expect(renderObject.owner, equals(tester.binding.pipelineOwner));
});
testWidgets('can manually attach RootWidget to build owner', (WidgetTester tester) async {
expect(find.byType(ColoredBox), findsNothing);
final RootWidget rootWidget = RootWidget(
child: View(
view: tester.view,
child: const ColoredBox(color: Colors.orange),
),
);
tester.binding.attachToBuildOwner(rootWidget);
await tester.pump();
expect(find.byType(ColoredBox), findsOneWidget);
expect(tester.binding.rootElement!.widget, equals(rootWidget));
expect(tester.element(find.byType(ColoredBox)).owner, equals(tester.binding.buildOwner));
});
}

View File

@ -0,0 +1,221 @@
// 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';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Widgets in view update as expected', (WidgetTester tester) async {
final Widget widget = View(
view: tester.view,
child: const TestWidget(),
);
await pumpWidgetWithoutViewWrapper(
tester: tester,
widget: widget,
);
expect(find.text('Hello'), findsOneWidget);
expect(tester.renderObject<RenderParagraph>(find.byType(Text)).text.toPlainText(), 'Hello');
tester.state<TestWidgetState>(find.byType(TestWidget)).text = 'World';
await tester.pump();
expect(find.text('Hello'), findsNothing);
expect(find.text('World'), findsOneWidget);
expect(tester.renderObject<RenderParagraph>(find.byType(Text)).text.toPlainText(), 'World');
await pumpWidgetWithoutViewWrapper(
tester: tester,
widget: ViewCollection(
views: <Widget>[widget],
),
);
expect(find.text('Hello'), findsNothing);
expect(find.text('World'), findsOneWidget);
expect(tester.renderObject<RenderParagraph>(find.byType(Text)).text.toPlainText(), 'World');
tester.state<TestWidgetState>(find.byType(TestWidget)).text = 'FooBar';
await pumpWidgetWithoutViewWrapper(
tester: tester,
widget: widget,
);
expect(find.text('World'), findsNothing);
expect(find.text('FooBar'), findsOneWidget);
expect(tester.renderObject<RenderParagraph>(find.byType(Text)).text.toPlainText(), 'FooBar');
});
testWidgets('Views in ViewCollection update as expected', (WidgetTester tester) async {
Iterable<String> renderParagraphTexts() {
return tester.renderObjectList<RenderParagraph>(find.byType(Text)).map((RenderParagraph r) => r.text.toPlainText());
}
final Key key1 = UniqueKey();
final Key key2 = UniqueKey();
final Widget view1 = View(
view: tester.view,
child: TestWidget(key: key1),
);
final Widget view2 = View(
view: FakeView(tester.view),
child: TestWidget(key: key2),
);
await pumpWidgetWithoutViewWrapper(
tester: tester,
widget: ViewCollection(
views: <Widget>[view1, view2],
),
);
expect(find.text('Hello'), findsNWidgets(2));
expect(renderParagraphTexts(), <String>['Hello', 'Hello']);
tester.state<TestWidgetState>(find.byKey(key1)).text = 'Guten';
tester.state<TestWidgetState>(find.byKey(key2)).text = 'Tag';
await tester.pump();
expect(find.text('Hello'), findsNothing);
expect(find.text('Guten'), findsOneWidget);
expect(find.text('Tag'), findsOneWidget);
expect(renderParagraphTexts(), <String>['Guten', 'Tag']);
tester.state<TestWidgetState>(find.byKey(key2)).text = 'Abend';
await tester.pump();
expect(find.text('Tag'), findsNothing);
expect(find.text('Guten'), findsOneWidget);
expect(find.text('Abend'), findsOneWidget);
expect(renderParagraphTexts(), <String>['Guten', 'Abend']);
tester.state<TestWidgetState>(find.byKey(key2)).text = 'Morgen';
await pumpWidgetWithoutViewWrapper(
tester: tester,
widget: ViewCollection(
views: <Widget>[view1, ViewCollection(views: <Widget>[view2])],
),
);
expect(find.text('Abend'), findsNothing);
expect(find.text('Guten'), findsOneWidget);
expect(find.text('Morgen'), findsOneWidget);
expect(renderParagraphTexts(), <String>['Guten', 'Morgen']);
});
testWidgets('Views in ViewAnchor update as expected', (WidgetTester tester) async {
Iterable<String> renderParagraphTexts() {
return tester.renderObjectList<RenderParagraph>(find.byType(Text)).map((RenderParagraph r) => r.text.toPlainText());
}
final Key insideAnchoredViewKey = UniqueKey();
final Key outsideAnchoredViewKey = UniqueKey();
final Widget view = View(
view: FakeView(tester.view),
child: TestWidget(key: insideAnchoredViewKey),
);
await tester.pumpWidget(
ViewAnchor(
view: view,
child: TestWidget(key: outsideAnchoredViewKey),
),
);
expect(find.text('Hello'), findsNWidgets(2));
expect(renderParagraphTexts(), <String>['Hello', 'Hello']);
tester.state<TestWidgetState>(find.byKey(outsideAnchoredViewKey)).text = 'Guten';
tester.state<TestWidgetState>(find.byKey(insideAnchoredViewKey)).text = 'Tag';
await tester.pump();
expect(find.text('Hello'), findsNothing);
expect(find.text('Guten'), findsOneWidget);
expect(find.text('Tag'), findsOneWidget);
expect(renderParagraphTexts(), <String>['Guten', 'Tag']);
tester.state<TestWidgetState>(find.byKey(insideAnchoredViewKey)).text = 'Abend';
await tester.pump();
expect(find.text('Tag'), findsNothing);
expect(find.text('Guten'), findsOneWidget);
expect(find.text('Abend'), findsOneWidget);
expect(renderParagraphTexts(), <String>['Guten', 'Abend']);
tester.state<TestWidgetState>(find.byKey(outsideAnchoredViewKey)).text = 'Schönen';
await tester.pump();
expect(find.text('Guten'), findsNothing);
expect(find.text('Schönen'), findsOneWidget);
expect(find.text('Abend'), findsOneWidget);
expect(renderParagraphTexts(), <String>['Schönen', 'Abend']);
tester.state<TestWidgetState>(find.byKey(insideAnchoredViewKey)).text = 'Tag';
await tester.pumpWidget(
ViewAnchor(
view: ViewCollection(views: <Widget>[view]),
child: TestWidget(key: outsideAnchoredViewKey),
),
);
await tester.pump();
expect(find.text('Abend'), findsNothing);
expect(find.text('Schönen'), findsOneWidget);
expect(find.text('Tag'), findsOneWidget);
expect(renderParagraphTexts(), <String>['Schönen', 'Tag']);
tester.state<TestWidgetState>(find.byKey(insideAnchoredViewKey)).text = 'Morgen';
await tester.pumpWidget(
SizedBox(
child: ViewAnchor(
view: ViewCollection(views: <Widget>[view]),
child: TestWidget(key: outsideAnchoredViewKey),
),
),
);
await tester.pump();
expect(find.text('Schönen'), findsNothing); // The `outsideAnchoredViewKey` is not a global key, its state is lost in the move above.
expect(find.text('Tag'), findsNothing);
expect(find.text('Hello'), findsOneWidget);
expect(find.text('Morgen'), findsOneWidget);
expect(renderParagraphTexts(), <String>['Hello', 'Morgen']);
});
}
class TestWidget extends StatefulWidget {
const TestWidget({super.key});
@override
State<TestWidget> createState() => TestWidgetState();
}
class TestWidgetState extends State<TestWidget> {
String get text => _text;
String _text = 'Hello';
set text(String value) {
if (_text == value) {
return;
}
setState(() {
_text = value;
});
}
@override
Widget build(BuildContext context) {
return Text(text, textDirection: TextDirection.ltr);
}
}
Future<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) {
tester.binding.attachRootWidget(widget);
tester.binding.scheduleFrame();
return tester.binding.pump();
}
class FakeView extends TestFlutterView{
FakeView(FlutterView view, { this.viewId = 100 }) : super(
view: view,
platformDispatcher: view.platformDispatcher as TestPlatformDispatcher,
display: view.display as TestDisplay,
);
@override
final int viewId;
}

View File

@ -222,16 +222,18 @@ void main() {
equalsIgnoringHashCodes( equalsIgnoringHashCodes(
'_RenderDiagonal#00000 relayoutBoundary=up1\n' '_RenderDiagonal#00000 relayoutBoundary=up1\n'
' │ creator: _Diagonal ← Align ← Directionality ← MediaQuery ←\n' ' │ creator: _Diagonal ← Align ← Directionality ← MediaQuery ←\n'
' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n' ' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n'
' │ TestFlutterView#00000] ← [root]\n' ' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
' │ [root]\n'
' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n' ' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n' ' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n'
' │ size: Size(190.0, 220.0)\n' ' │ size: Size(190.0, 220.0)\n'
'\n' '\n'
' ├─topLeft: RenderConstrainedBox#00000 relayoutBoundary=up2\n' ' ├─topLeft: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
' │ creator: SizedBox ← _Diagonal ← Align ← Directionality ←\n' ' │ creator: SizedBox ← _Diagonal ← Align ← Directionality ←\n'
' │ MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n' ' │ MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope ←\n'
' │ View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n' ' │ _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
' │ TestFlutterView#00000] ← View ← [root]\n'
' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n' ' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
' │ constraints: BoxConstraints(unconstrained)\n' ' │ constraints: BoxConstraints(unconstrained)\n'
' │ size: Size(80.0, 100.0)\n' ' │ size: Size(80.0, 100.0)\n'
@ -239,8 +241,9 @@ void main() {
'\n' '\n'
' └─bottomRight: RenderConstrainedBox#00000 relayoutBoundary=up2\n' ' └─bottomRight: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
' creator: SizedBox ← _Diagonal ← Align ← Directionality ←\n' ' creator: SizedBox ← _Diagonal ← Align ← Directionality ←\n'
' MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n' ' MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope ←\n'
' View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n' ' _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
' TestFlutterView#00000] ← View ← [root]\n'
' parentData: offset=Offset(80.0, 100.0) (can use size)\n' ' parentData: offset=Offset(80.0, 100.0) (can use size)\n'
' constraints: BoxConstraints(unconstrained)\n' ' constraints: BoxConstraints(unconstrained)\n'
' size: Size(110.0, 120.0)\n' ' size: Size(110.0, 120.0)\n'

View File

@ -10,10 +10,9 @@ import 'test_widgets.dart';
void main() { void main() {
testWidgets('Stateful widget smoke test', (WidgetTester tester) async { testWidgets('Stateful widget smoke test', (WidgetTester tester) async {
void checkTree(BoxDecoration expectedDecoration) { void checkTree(BoxDecoration expectedDecoration) {
final SingleChildRenderObjectElement element = tester.element( final SingleChildRenderObjectElement element = tester.element(
find.byElementPredicate((Element element) => element is SingleChildRenderObjectElement), find.byElementPredicate((Element element) => element is SingleChildRenderObjectElement && element.renderObject is! RenderView),
); );
expect(element, isNotNull); expect(element, isNotNull);
expect(element.renderObject, isA<RenderDecoratedBox>()); expect(element.renderObject, isA<RenderDecoratedBox>());

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,8 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/widgets.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
@ -67,4 +68,436 @@ void main() {
)), )),
); );
}); });
testWidgets('child of view finds view, parentPipelineOwner, mediaQuery', (WidgetTester tester) async {
FlutterView? outsideView;
FlutterView? insideView;
PipelineOwner? outsideParent;
PipelineOwner? insideParent;
await pumpWidgetWithoutViewWrapper(
tester: tester,
widget: Builder(
builder: (BuildContext context) {
outsideView = View.maybeOf(context);
outsideParent = View.pipelineOwnerOf(context);
return View(
view: tester.view,
child: Builder(
builder: (BuildContext context) {
insideView = View.maybeOf(context);
insideParent = View.pipelineOwnerOf(context);
return const SizedBox();
},
),
);
},
),
);
expect(outsideView, isNull);
expect(insideView, equals(tester.view));
expect(outsideParent, isNotNull);
expect(insideParent, isNotNull);
expect(outsideParent, isNot(equals(insideParent)));
expect(outsideParent, tester.binding.rootPipelineOwner);
expect(insideParent, equals(tester.renderObject(find.byType(SizedBox)).owner));
final List<PipelineOwner> pipelineOwners = <PipelineOwner> [];
tester.binding.rootPipelineOwner.visitChildren((PipelineOwner child) {
pipelineOwners.add(child);
});
expect(pipelineOwners.single, equals(insideParent));
});
testWidgets('cannot have multiple views with same FlutterView', (WidgetTester tester) async {
await pumpWidgetWithoutViewWrapper(
tester: tester,
widget: ViewCollection(
views: <Widget>[
View(
view: tester.view,
child: const SizedBox(),
),
View(
view: tester.view,
child: const SizedBox(),
),
],
),
);
expect(
tester.takeException(),
isFlutterError.having(
(FlutterError e) => e.message,
'message',
contains('Multiple widgets used the same GlobalKey'),
),
);
});
testWidgets('ViewCollection must have one view', (WidgetTester tester) async {
expect(() => ViewCollection(views: const <Widget>[]), throwsAssertionError);
});
testWidgets('ViewAnchor.child does not see surrounding view', (WidgetTester tester) async {
FlutterView? inside;
FlutterView? outside;
await tester.pumpWidget(
Builder(
builder: (BuildContext context) {
outside = View.maybeOf(context);
return ViewAnchor(
view: Builder(
builder: (BuildContext context) {
inside = View.maybeOf(context);
return View(view: FakeView(tester.view), child: const SizedBox());
},
),
child: const SizedBox(),
);
},
),
);
expect(inside, isNull);
expect(outside, isNotNull);
});
testWidgets('ViewAnchor layout order', (WidgetTester tester) async {
Finder findSpyWidget(int label) {
return find.byWidgetPredicate((Widget w) => w is SpyRenderWidget && w.label == label);
}
final List<String> log = <String>[];
await tester.pumpWidget(
SpyRenderWidget(
label: 1,
log: log,
child: ViewAnchor(
view: View(
view: FakeView(tester.view),
child: SpyRenderWidget(label: 2, log: log),
),
child: SpyRenderWidget(label: 3, log: log),
),
),
);
log.clear();
tester.renderObject(findSpyWidget(3)).markNeedsLayout();
tester.renderObject(findSpyWidget(2)).markNeedsLayout();
tester.renderObject(findSpyWidget(1)).markNeedsLayout();
await tester.pump();
expect(log, <String>['layout 1', 'layout 3', 'layout 2']);
});
testWidgets('visitChildren of ViewAnchor visits both children', (WidgetTester tester) async {
await tester.pumpWidget(
ViewAnchor(
view: View(
view: FakeView(tester.view),
child: const ColoredBox(color: Colors.green),
),
child: const SizedBox(),
),
);
final Element viewAnchorElement = tester.element(find.byElementPredicate((Element e) => e.runtimeType.toString() == '_MultiChildComponentElement'));
final List<Element> children = <Element>[];
viewAnchorElement.visitChildren((Element element) {
children.add(element);
});
expect(children, hasLength(2));
await tester.pumpWidget(
const ViewAnchor(
child: SizedBox(),
),
);
children.clear();
viewAnchorElement.visitChildren((Element element) {
children.add(element);
});
expect(children, hasLength(1));
});
testWidgets('visitChildren of ViewCollection visits all children', (WidgetTester tester) async {
await pumpWidgetWithoutViewWrapper(
tester: tester,
widget: ViewCollection(
views: <Widget>[
View(
view: tester.view,
child: const SizedBox(),
),
View(
view: FakeView(tester.view),
child: const SizedBox(),
),
View(
view: FakeView(tester.view, viewId: 423),
child: const SizedBox(),
),
],
),
);
final Element viewAnchorElement = tester.element(find.byElementPredicate((Element e) => e.runtimeType.toString() == '_MultiChildComponentElement'));
final List<Element> children = <Element>[];
viewAnchorElement.visitChildren((Element element) {
children.add(element);
});
expect(children, hasLength(3));
await pumpWidgetWithoutViewWrapper(
tester: tester,
widget: ViewCollection(
views: <Widget>[
View(
view: tester.view,
child: const SizedBox(),
),
],
),
);
children.clear();
viewAnchorElement.visitChildren((Element element) {
children.add(element);
});
expect(children, hasLength(1));
});
group('renderObject getter', () {
testWidgets('ancestors of view see RenderView as renderObject', (WidgetTester tester) async {
late BuildContext builderContext;
await pumpWidgetWithoutViewWrapper(
tester: tester,
widget: Builder(
builder: (BuildContext context) {
builderContext = context;
return View(
view: tester.view,
child: const SizedBox(),
);
},
),
);
final RenderObject? renderObject = builderContext.findRenderObject();
expect(renderObject, isNotNull);
expect(renderObject, isA<RenderView>());
expect(renderObject, tester.renderObject(find.byType(View)));
expect(tester.element(find.byType(Builder)).renderObject, renderObject);
});
testWidgets('ancestors of ViewCollection get null for renderObject', (WidgetTester tester) async {
late BuildContext builderContext;
await pumpWidgetWithoutViewWrapper(
tester: tester,
widget: Builder(
builder: (BuildContext context) {
builderContext = context;
return ViewCollection(
views: <Widget>[
View(
view: tester.view,
child: const SizedBox(),
),
View(
view: FakeView(tester.view),
child: const SizedBox(),
),
],
);
},
),
);
final RenderObject? renderObject = builderContext.findRenderObject();
expect(renderObject, isNull);
expect(tester.element(find.byType(Builder)).renderObject, isNull);
});
testWidgets('ancestors of a ViewAnchor see the right RenderObject', (WidgetTester tester) async {
late BuildContext builderContext;
await tester.pumpWidget(
Builder(
builder: (BuildContext context) {
builderContext = context;
return ViewAnchor(
view: View(
view: FakeView(tester.view),
child: const ColoredBox(color: Colors.green),
),
child: const SizedBox(),
);
},
),
);
final RenderObject? renderObject = builderContext.findRenderObject();
expect(renderObject, isNotNull);
expect(renderObject, isA<RenderConstrainedBox>());
expect(renderObject, tester.renderObject(find.byType(SizedBox)));
expect(tester.element(find.byType(Builder)).renderObject, renderObject);
});
});
testWidgets('correctly switches between view configurations', (WidgetTester tester) async {
await pumpWidgetWithoutViewWrapper(
tester: tester,
widget: View(
view: tester.view,
deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner,
deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView,
child: const SizedBox(),
),
);
RenderObject renderView = tester.renderObject(find.byType(View));
expect(renderView, same(tester.binding.renderView));
expect(renderView.owner, same(tester.binding.pipelineOwner));
expect(tester.renderObject(find.byType(SizedBox)).owner, same(tester.binding.pipelineOwner));
await pumpWidgetWithoutViewWrapper(
tester: tester,
widget: View(
view: tester.view,
child: const SizedBox(),
),
);
renderView = tester.renderObject(find.byType(View));
expect(renderView, isNot(same(tester.binding.renderView)));
expect(renderView.owner, isNot(same(tester.binding.pipelineOwner)));
expect(tester.renderObject(find.byType(SizedBox)).owner, isNot(same(tester.binding.pipelineOwner)));
await pumpWidgetWithoutViewWrapper(
tester: tester,
widget: View(
view: tester.view,
deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner,
deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView,
child: const SizedBox(),
),
);
renderView = tester.renderObject(find.byType(View));
expect(renderView, same(tester.binding.renderView));
expect(renderView.owner, same(tester.binding.pipelineOwner));
expect(tester.renderObject(find.byType(SizedBox)).owner, same(tester.binding.pipelineOwner));
expect(() => View(
view: tester.view,
deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner,
child: const SizedBox(),
), throwsAssertionError);
expect(() => View(
view: tester.view,
deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView,
child: const SizedBox(),
), throwsAssertionError);
expect(() => View(
view: FakeView(tester.view),
deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView,
deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner,
child: const SizedBox(),
), throwsAssertionError);
});
testWidgets('attaches itself correctly', (WidgetTester tester) async {
final Key viewKey = UniqueKey();
late final PipelineOwner parentPipelineOwner;
await tester.pumpWidget(
ViewAnchor(
view: Builder(
builder: (BuildContext context) {
parentPipelineOwner = View.pipelineOwnerOf(context);
return View(
key: viewKey,
view: FakeView(tester.view),
child: const SizedBox(),
);
},
),
child: const ColoredBox(color: Colors.green),
),
);
expect(parentPipelineOwner, isNot(RendererBinding.instance.rootPipelineOwner));
final RenderView rawView = tester.renderObject<RenderView>(find.byKey(viewKey));
expect(RendererBinding.instance.renderViews, contains(rawView));
final List<PipelineOwner> children = <PipelineOwner>[];
parentPipelineOwner.visitChildren((PipelineOwner child) {
children.add(child);
});
final PipelineOwner rawViewOwner = rawView.owner!;
expect(children, contains(rawViewOwner));
// Remove that View from the tree.
await tester.pumpWidget(
const ViewAnchor(
child: ColoredBox(color: Colors.green),
),
);
expect(rawView.owner, isNull);
expect(RendererBinding.instance.renderViews, isNot(contains(rawView)));
children.clear();
parentPipelineOwner.visitChildren((PipelineOwner child) {
children.add(child);
});
expect(children, isNot(contains(rawViewOwner)));
});
}
Future<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) {
tester.binding.attachRootWidget(widget);
tester.binding.scheduleFrame();
return tester.binding.pump();
}
class FakeView extends TestFlutterView{
FakeView(FlutterView view, { this.viewId = 100 }) : super(
view: view,
platformDispatcher: view.platformDispatcher as TestPlatformDispatcher,
display: view.display as TestDisplay,
);
@override
final int viewId;
}
class SpyRenderWidget extends SizedBox {
const SpyRenderWidget({super.key, required this.label, required this.log, super.child});
final int label;
final List<String> log;
@override
RenderSpy createRenderObject(BuildContext context) {
return RenderSpy(
additionalConstraints: const BoxConstraints(),
label: label,
log: log,
);
}
@override
void updateRenderObject(BuildContext context, RenderSpy renderObject) {
renderObject
..label = label
..log = log;
}
}
class RenderSpy extends RenderConstrainedBox {
RenderSpy({required super.additionalConstraints, required this.label, required this.log});
int label;
List<String> log;
@override
void performLayout() {
log.add('layout $label');
super.performLayout();
}
} }

View File

@ -4509,7 +4509,6 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
expect(result['parentData'], isNull); expect(result['parentData'], isNull);
}); });
testWidgets('ext.flutter.inspector.getLayoutExplorerNode for RenderView',(WidgetTester tester) async { testWidgets('ext.flutter.inspector.getLayoutExplorerNode for RenderView',(WidgetTester tester) async {
await pumpWidgetForLayoutExplorer(tester); await pumpWidgetForLayoutExplorer(tester);
@ -4530,7 +4529,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
final Map<String, Object?>? renderObject = result['renderObject'] as Map<String, Object?>?; final Map<String, Object?>? renderObject = result['renderObject'] as Map<String, Object?>?;
expect(renderObject, isNotNull); expect(renderObject, isNotNull);
expect(renderObject!['description'], startsWith('RenderView')); expect(renderObject!['description'], contains('RenderView'));
expect(result['parentRenderElement'], isNull); expect(result['parentRenderElement'], isNull);
expect(result['constraints'], isNull); expect(result['constraints'], isNull);

View File

@ -55,7 +55,7 @@ class _TestRenderObject extends RenderObject {
Rect get semanticBounds => throw UnimplementedError(); Rect get semanticBounds => throw UnimplementedError();
} }
class _TestElement extends RenderObjectElement with RootElementMixin { class _TestElement extends RenderTreeRootElement with RootElementMixin {
_TestElement(): super(_TestLeafRenderObjectWidget()); _TestElement(): super(_TestLeafRenderObjectWidget());
void makeInactive() { void makeInactive() {

View File

@ -187,11 +187,20 @@ mixin CommandHandlerFactory {
Future<Health> _getHealth(Command command) async => const Health(HealthStatus.ok); Future<Health> _getHealth(Command command) async => const Health(HealthStatus.ok);
Future<LayerTree> _getLayerTree(Command command) async { Future<LayerTree> _getLayerTree(Command command) async {
return LayerTree(RendererBinding.instance.renderView.debugLayer?.toStringDeep()); final String trees = <String>[
for (final RenderView renderView in RendererBinding.instance.renderViews)
if (renderView.debugLayer != null)
renderView.debugLayer!.toStringDeep(),
].join('\n\n');
return LayerTree(trees.isNotEmpty ? trees : null);
} }
Future<RenderTree> _getRenderTree(Command command) async { Future<RenderTree> _getRenderTree(Command command) async {
return RenderTree(RendererBinding.instance.renderView.toStringDeep()); final String trees = <String>[
for (final RenderView renderView in RendererBinding.instance.renderViews)
renderView.toStringDeep(),
].join('\n\n');
return RenderTree(trees.isNotEmpty ? trees : null);
} }
Future<Result> _enterText(Command command) async { Future<Result> _enterText(Command command) async {

View File

@ -58,8 +58,8 @@ class MatchesGoldenFile extends AsyncMatcher {
final RenderObject renderObject = _findRepaintBoundary(element); final RenderObject renderObject = _findRepaintBoundary(element);
final Size size = renderObject.paintBounds.size; final Size size = renderObject.paintBounds.size;
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance; final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance;
final Element e = binding.rootElement!;
final ui.FlutterView view = binding.platformDispatcher.implicitView!; final ui.FlutterView view = binding.platformDispatcher.implicitView!;
final RenderView renderView = binding.renderViews.firstWhere((RenderView r) => r.flutterView == view);
// Unlike `flutter_tester`, we don't have the ability to render an element // Unlike `flutter_tester`, we don't have the ability to render an element
// to an image directly. Instead, we will use `window.render()` to render // to an image directly. Instead, we will use `window.render()` to render
@ -78,7 +78,7 @@ class MatchesGoldenFile extends AsyncMatcher {
return ex.message; return ex.message;
} }
}); });
_renderElement(view, _findRepaintBoundary(e)); _renderElement(view, renderView);
return result; return result;
} }

View File

@ -131,11 +131,10 @@ class MinimumTapTargetGuideline extends AccessibilityGuideline {
@override @override
FutureOr<Evaluation> evaluate(WidgetTester tester) { FutureOr<Evaluation> evaluate(WidgetTester tester) {
Evaluation result = const Evaluation.pass(); Evaluation result = const Evaluation.pass();
for (final FlutterView view in tester.platformDispatcher.views) { for (final RenderView view in tester.binding.renderViews) {
result += _traverse( result += _traverse(
view, view.flutterView,
// TODO(pdblasi-google): Get the specific semantics root for this view when available view.owner!.semanticsOwner!.rootSemanticsNode!,
tester.binding.pipelineOwner.semanticsOwner!.rootSemanticsNode!,
); );
} }
@ -239,10 +238,8 @@ class LabeledTapTargetGuideline extends AccessibilityGuideline {
FutureOr<Evaluation> evaluate(WidgetTester tester) { FutureOr<Evaluation> evaluate(WidgetTester tester) {
Evaluation result = const Evaluation.pass(); Evaluation result = const Evaluation.pass();
// TODO(pdblasi-google): Use view to retrieve the appropriate root semantics node when available. for (final RenderView view in tester.binding.renderViews) {
// ignore: unused_local_variable result += _traverse(view.owner!.semanticsOwner!.rootSemanticsNode!);
for (final FlutterView view in tester.platformDispatcher.views) {
result += _traverse(tester.binding.pipelineOwner.semanticsOwner!.rootSemanticsNode!);
} }
return result; return result;
@ -318,9 +315,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
@override @override
Future<Evaluation> evaluate(WidgetTester tester) async { Future<Evaluation> evaluate(WidgetTester tester) async {
Evaluation result = const Evaluation.pass(); Evaluation result = const Evaluation.pass();
for (final FlutterView view in tester.platformDispatcher.views) { for (final RenderView renderView in tester.binding.renderViews) {
// TODO(pdblasi): This renderView will need to be retrieved from view when available.
final RenderView renderView = tester.binding.renderView;
final OffsetLayer layer = renderView.debugLayer! as OffsetLayer; final OffsetLayer layer = renderView.debugLayer! as OffsetLayer;
final SemanticsNode root = renderView.owner!.semanticsOwner!.rootSemanticsNode!; final SemanticsNode root = renderView.owner!.semanticsOwner!.rootSemanticsNode!;
@ -329,13 +324,13 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
() async { () async {
// Needs to be the same pixel ratio otherwise our dimensions won't match // Needs to be the same pixel ratio otherwise our dimensions won't match
// the last transform layer. // the last transform layer.
final double ratio = 1 / view.devicePixelRatio; final double ratio = 1 / renderView.flutterView.devicePixelRatio;
image = await layer.toImage(renderView.paintBounds, pixelRatio: ratio); image = await layer.toImage(renderView.paintBounds, pixelRatio: ratio);
return image.toByteData(); return image.toByteData();
}, },
); );
result += await _evaluateNode(root, tester, image, byteData!, view); result += await _evaluateNode(root, tester, image, byteData!, renderView);
} }
return result; return result;
@ -346,7 +341,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
WidgetTester tester, WidgetTester tester,
ui.Image image, ui.Image image,
ByteData byteData, ByteData byteData,
FlutterView view, RenderView renderView,
) async { ) async {
Evaluation result = const Evaluation.pass(); Evaluation result = const Evaluation.pass();
@ -368,7 +363,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
return true; return true;
}); });
for (final SemanticsNode child in children) { for (final SemanticsNode child in children) {
result += await _evaluateNode(child, tester, image, byteData, view); result += await _evaluateNode(child, tester, image, byteData, renderView);
} }
if (shouldSkipNode(data)) { if (shouldSkipNode(data)) {
return result; return result;
@ -376,7 +371,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
final String text = data.label.isEmpty ? data.value : data.label; final String text = data.label.isEmpty ? data.value : data.label;
final Iterable<Element> elements = find.text(text).hitTestable().evaluate(); final Iterable<Element> elements = find.text(text).hitTestable().evaluate();
for (final Element element in elements) { for (final Element element in elements) {
result += await _evaluateElement(node, element, tester, image, byteData, view); result += await _evaluateElement(node, element, tester, image, byteData, renderView);
} }
return result; return result;
} }
@ -387,7 +382,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
WidgetTester tester, WidgetTester tester,
ui.Image image, ui.Image image,
ByteData byteData, ByteData byteData,
FlutterView view, RenderView renderView,
) async { ) async {
// Look up inherited text properties to determine text size and weight. // Look up inherited text properties to determine text size and weight.
late bool isBold; late bool isBold;
@ -408,7 +403,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
// not included in renderBox.getTransformTo(null). Manually multiply the // not included in renderBox.getTransformTo(null). Manually multiply the
// root transform to the global transform. // root transform to the global transform.
final Matrix4 rootTransform = Matrix4.identity(); final Matrix4 rootTransform = Matrix4.identity();
tester.binding.renderView.applyPaintTransform(tester.binding.renderView.child!, rootTransform); renderView.applyPaintTransform(renderView.child!, rootTransform);
rootTransform.multiply(globalTransform); rootTransform.multiply(globalTransform);
screenBounds = MatrixUtils.transformRect(rootTransform, renderBox.paintBounds); screenBounds = MatrixUtils.transformRect(rootTransform, renderBox.paintBounds);
Rect nodeBounds = node.rect; Rect nodeBounds = node.rect;
@ -443,7 +438,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
throw StateError('Unexpected widget type: ${widget.runtimeType}'); throw StateError('Unexpected widget type: ${widget.runtimeType}');
} }
if (isNodeOffScreen(paintBoundsWithOffset, view)) { if (isNodeOffScreen(paintBoundsWithOffset, renderView.flutterView)) {
return const Evaluation.pass(); return const Evaluation.pass();
} }
@ -562,9 +557,7 @@ class CustomMinimumContrastGuideline extends AccessibilityGuideline {
Evaluation result = const Evaluation.pass(); Evaluation result = const Evaluation.pass();
for (final Element element in elements) { for (final Element element in elements) {
final FlutterView view = tester.viewOf(find.byElementPredicate((Element e) => e == element)); final FlutterView view = tester.viewOf(find.byElementPredicate((Element e) => e == element));
final RenderView renderView = tester.binding.renderViews.firstWhere((RenderView r) => r.flutterView == view);
// TODO(pdblasi): Obtain this renderView from view when possible.
final RenderView renderView = tester.binding.renderView;
final OffsetLayer layer = renderView.debugLayer! as OffsetLayer; final OffsetLayer layer = renderView.debugLayer! as OffsetLayer;
late final ui.Image image; late final ui.Image image;

View File

@ -495,8 +495,8 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
Size? _surfaceSize; Size? _surfaceSize;
/// Artificially changes the surface size to `size` on the Widget binding, /// Artificially changes the logical size of [WidgetTester.view] to the
/// then flushes microtasks. /// specified size, then flushes microtasks.
/// ///
/// Set to null to use the default surface size. /// Set to null to use the default surface size.
/// ///
@ -508,7 +508,10 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
/// addTearDown(() => binding.setSurfaceSize(null)); /// addTearDown(() => binding.setSurfaceSize(null));
/// ``` /// ```
/// ///
/// See also [TestFlutterView.physicalSize], which has a similar effect. /// This method only affects the size of the [WidgetTester.view]. It does not
/// affect the size of any other views. Instead of this method, consider
/// setting [TestFlutterView.physicalSize], which works for any view,
/// including [WidgetTester.view].
// TODO(pdblasi-google): Deprecate this. https://github.com/flutter/flutter/issues/123881 // TODO(pdblasi-google): Deprecate this. https://github.com/flutter/flutter/issues/123881
Future<void> setSurfaceSize(Size? size) { Future<void> setSurfaceSize(Size? size) {
return TestAsyncUtils.guard<void>(() async { return TestAsyncUtils.guard<void>(() async {
@ -522,15 +525,38 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
} }
@override @override
ViewConfiguration createViewConfiguration() { void addRenderView(RenderView view) {
final FlutterView view = platformDispatcher.implicitView!; _insideAddRenderView = true;
final double devicePixelRatio = view.devicePixelRatio; try {
final Size size = _surfaceSize ?? view.physicalSize / devicePixelRatio; super.addRenderView(view);
} finally {
_insideAddRenderView = false;
}
}
bool _insideAddRenderView = false;
@override
ViewConfiguration createViewConfigurationFor(RenderView renderView) {
if (_insideAddRenderView
&& renderView.hasConfiguration
&& renderView.configuration is TestViewConfiguration
&& renderView == this.renderView) { // ignore: deprecated_member_use
// If a test has reached out to the now deprecated renderView property to set a custom TestViewConfiguration
// we are not replacing it. This is to maintain backwards compatibility with how things worked prior to the
// deprecation of that property.
// TODO(goderbauer): Remove this "if" when the deprecated renderView property is removed.
return renderView.configuration;
}
final FlutterView view = renderView.flutterView;
if (_surfaceSize != null && view == platformDispatcher.implicitView) {
return ViewConfiguration( return ViewConfiguration(
size: size, size: _surfaceSize!,
devicePixelRatio: devicePixelRatio, devicePixelRatio: view.devicePixelRatio,
); );
} }
return super.createViewConfigurationFor(renderView);
}
/// Acts as if the application went idle. /// Acts as if the application went idle.
/// ///
@ -1377,16 +1403,18 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
debugBuildingDirtyElements = true; debugBuildingDirtyElements = true;
buildOwner!.buildScope(rootElement!); buildOwner!.buildScope(rootElement!);
if (_phase != EnginePhase.build) { if (_phase != EnginePhase.build) {
pipelineOwner.flushLayout(); rootPipelineOwner.flushLayout();
if (_phase != EnginePhase.layout) { if (_phase != EnginePhase.layout) {
pipelineOwner.flushCompositingBits(); rootPipelineOwner.flushCompositingBits();
if (_phase != EnginePhase.compositingBits) { if (_phase != EnginePhase.compositingBits) {
pipelineOwner.flushPaint(); rootPipelineOwner.flushPaint();
if (_phase != EnginePhase.paint && sendFramesToEngine) { if (_phase != EnginePhase.paint && sendFramesToEngine) {
_firstFrameSent = true; _firstFrameSent = true;
for (final RenderView renderView in renderViews) {
renderView.compositeFrame(); // this sends the bits to the GPU renderView.compositeFrame(); // this sends the bits to the GPU
}
if (_phase != EnginePhase.composite) { if (_phase != EnginePhase.composite) {
pipelineOwner.flushSemantics(); rootPipelineOwner.flushSemantics(); // this sends the semantics to the OS.
assert(_phase == EnginePhase.flushSemantics || assert(_phase == EnginePhase.flushSemantics ||
_phase == EnginePhase.sendSemanticsUpdate); _phase == EnginePhase.sendSemanticsUpdate);
} }
@ -1759,10 +1787,15 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
} }
} }
void _markViewNeedsPaint() { void _markViewsNeedPaint([int? viewId]) {
_viewNeedsPaint = true; _viewNeedsPaint = true;
final Iterable<RenderView> toMark = viewId == null
? renderViews
: renderViews.where((RenderView renderView) => renderView.flutterView.viewId == viewId);
for (final RenderView renderView in toMark) {
renderView.markNeedsPaint(); renderView.markNeedsPaint();
} }
}
TextPainter? _label; TextPainter? _label;
static const TextStyle _labelStyle = TextStyle( static const TextStyle _labelStyle = TextStyle(
@ -1779,15 +1812,16 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
_label ??= TextPainter(textAlign: TextAlign.left, textDirection: TextDirection.ltr); _label ??= TextPainter(textAlign: TextAlign.left, textDirection: TextDirection.ltr);
_label!.text = TextSpan(text: value, style: _labelStyle); _label!.text = TextSpan(text: value, style: _labelStyle);
_label!.layout(); _label!.layout();
_markViewNeedsPaint(); _markViewsNeedPaint();
} }
final Map<int, _LiveTestPointerRecord> _pointerIdToPointerRecord = <int, _LiveTestPointerRecord>{}; final Expando<Map<int, _LiveTestPointerRecord>> _renderViewToPointerIdToPointerRecord = Expando<Map<int, _LiveTestPointerRecord>>();
void _handleRenderViewPaint(PaintingContext context, Offset offset, RenderView renderView) { void _handleRenderViewPaint(PaintingContext context, Offset offset, RenderView renderView) {
assert(offset == Offset.zero); assert(offset == Offset.zero);
if (_pointerIdToPointerRecord.isNotEmpty) { final Map<int, _LiveTestPointerRecord>? pointerIdToRecord = _renderViewToPointerIdToPointerRecord[renderView];
if (pointerIdToRecord != null && pointerIdToRecord.isNotEmpty) {
final double radius = renderView.configuration.size.shortestSide * 0.05; final double radius = renderView.configuration.size.shortestSide * 0.05;
final Path path = Path() final Path path = Path()
..addOval(Rect.fromCircle(center: Offset.zero, radius: radius)) ..addOval(Rect.fromCircle(center: Offset.zero, radius: radius))
@ -1800,7 +1834,7 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
..strokeWidth = radius / 10.0 ..strokeWidth = radius / 10.0
..style = PaintingStyle.stroke; ..style = PaintingStyle.stroke;
bool dirty = false; bool dirty = false;
for (final _LiveTestPointerRecord record in _pointerIdToPointerRecord.values) { for (final _LiveTestPointerRecord record in pointerIdToRecord.values) {
paint.color = record.color.withOpacity(record.decay < 0 ? (record.decay / (_kPointerDecay - 1)) : 1.0); paint.color = record.color.withOpacity(record.decay < 0 ? (record.decay / (_kPointerDecay - 1)) : 1.0);
canvas.drawPath(path.shift(record.position), paint); canvas.drawPath(path.shift(record.position), paint);
if (record.decay < 0) { if (record.decay < 0) {
@ -1808,14 +1842,14 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
} }
record.decay += 1; record.decay += 1;
} }
_pointerIdToPointerRecord pointerIdToRecord
.keys .keys
.where((int pointer) => _pointerIdToPointerRecord[pointer]!.decay == 0) .where((int pointer) => pointerIdToRecord[pointer]!.decay == 0)
.toList() .toList()
.forEach(_pointerIdToPointerRecord.remove); .forEach(pointerIdToRecord.remove);
if (dirty) { if (dirty) {
scheduleMicrotask(() { scheduleMicrotask(() {
_markViewNeedsPaint(); _markViewsNeedPaint(renderView.flutterView.viewId);
}); });
} }
} }
@ -1846,19 +1880,29 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
void handlePointerEvent(PointerEvent event) { void handlePointerEvent(PointerEvent event) {
switch (pointerEventSource) { switch (pointerEventSource) {
case TestBindingEventSource.test: case TestBindingEventSource.test:
final _LiveTestPointerRecord? record = _pointerIdToPointerRecord[event.pointer]; RenderView? target;
for (final RenderView renderView in renderViews) {
if (renderView.flutterView.viewId == event.viewId) {
target = renderView;
break;
}
}
if (target != null) {
final _LiveTestPointerRecord? record = _renderViewToPointerIdToPointerRecord[target]?[event.pointer];
if (record != null) { if (record != null) {
record.position = event.position; record.position = event.position;
if (!event.down) { if (!event.down) {
record.decay = _kPointerDecay; record.decay = _kPointerDecay;
} }
_markViewNeedsPaint(); _markViewsNeedPaint(event.viewId);
} else if (event.down) { } else if (event.down) {
_pointerIdToPointerRecord[event.pointer] = _LiveTestPointerRecord( _renderViewToPointerIdToPointerRecord[target] ??= <int, _LiveTestPointerRecord>{};
_renderViewToPointerIdToPointerRecord[target]![event.pointer] = _LiveTestPointerRecord(
event.pointer, event.pointer,
event.position, event.position,
); );
_markViewNeedsPaint(); _markViewsNeedPaint(event.viewId);
}
} }
super.handlePointerEvent(event); super.handlePointerEvent(event);
case TestBindingEventSource.device: case TestBindingEventSource.device:
@ -1870,6 +1914,7 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
// The pointer events received with this source has a global position // The pointer events received with this source has a global position
// (see [handlePointerEventForSource]). Transform it to the local // (see [handlePointerEventForSource]). Transform it to the local
// coordinate space used by the testing widgets. // coordinate space used by the testing widgets.
final RenderView renderView = renderViews.firstWhere((RenderView r) => r.flutterView.viewId == event.viewId);
final PointerEvent localEvent = event.copyWith(position: globalToLocal(event.position, renderView)); final PointerEvent localEvent = event.copyWith(position: globalToLocal(event.position, renderView));
withPointerEventSource(TestBindingEventSource.device, withPointerEventSource(TestBindingEventSource.device,
() => super.handlePointerEvent(localEvent) () => super.handlePointerEvent(localEvent)
@ -1987,10 +2032,18 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
} }
@override @override
ViewConfiguration createViewConfiguration() { ViewConfiguration createViewConfigurationFor(RenderView renderView) {
final FlutterView view = renderView.flutterView;
if (view == platformDispatcher.implicitView) {
return TestViewConfiguration.fromView( return TestViewConfiguration.fromView(
size: _surfaceSize ?? _kDefaultTestViewportSize, size: _surfaceSize ?? _kDefaultTestViewportSize,
view: platformDispatcher.implicitView!, view: view,
);
}
final double devicePixelRatio = view.devicePixelRatio;
return TestViewConfiguration.fromView(
size: view.physicalSize / devicePixelRatio,
view: view,
); );
} }

View File

@ -36,7 +36,7 @@ class SemanticsController {
/// Creates a [SemanticsController] that uses the given binding. Will be /// Creates a [SemanticsController] that uses the given binding. Will be
/// automatically created as part of instantiating a [WidgetController], but /// automatically created as part of instantiating a [WidgetController], but
/// a custom implementation can be passed via the [WidgetController] constructor. /// a custom implementation can be passed via the [WidgetController] constructor.
SemanticsController._(WidgetsBinding binding) : _binding = binding; SemanticsController._(this._controller);
static final int _scrollingActions = static final int _scrollingActions =
SemanticsAction.scrollUp.index | SemanticsAction.scrollUp.index |
@ -55,7 +55,7 @@ class SemanticsController {
SemanticsFlag.isSlider.index | SemanticsFlag.isSlider.index |
SemanticsFlag.isInMutuallyExclusiveGroup.index; SemanticsFlag.isInMutuallyExclusiveGroup.index;
final WidgetsBinding _binding; final WidgetController _controller;
/// Attempts to find the [SemanticsNode] of first result from `finder`. /// Attempts to find the [SemanticsNode] of first result from `finder`.
/// ///
@ -73,7 +73,7 @@ class SemanticsController {
/// if no semantics are found or are not enabled. /// if no semantics are found or are not enabled.
SemanticsNode find(Finder finder) { SemanticsNode find(Finder finder) {
TestAsyncUtils.guardSync(); TestAsyncUtils.guardSync();
if (!_binding.semanticsEnabled) { if (!_controller.binding.semanticsEnabled) {
throw StateError('Semantics are not enabled.'); throw StateError('Semantics are not enabled.');
} }
final Iterable<Element> candidates = finder.evaluate(); final Iterable<Element> candidates = finder.evaluate();
@ -109,6 +109,13 @@ class SemanticsController {
/// tree. If `end` finds zero elements or more than one element, a /// tree. If `end` finds zero elements or more than one element, a
/// [StateError] will be thrown. /// [StateError] will be thrown.
/// ///
/// If provided, the nodes for `end` and `start` must be part of the same
/// semantics tree, i.e. they must be part of the same view.
///
/// If neither `start` or `end` is provided, `view` can be provided to specify
/// the semantics tree to traverse. If `view` is left unspecified,
/// [WidgetTester.view] is traversed by default.
///
/// Since the order is simulated, edge cases that differ between platforms /// Since the order is simulated, edge cases that differ between platforms
/// (such as how the last visible item in a scrollable list is handled) may be /// (such as how the last visible item in a scrollable list is handled) may be
/// inconsistent with platform behavior, but are expected to be sufficient for /// inconsistent with platform behavior, but are expected to be sufficient for
@ -139,10 +146,47 @@ class SemanticsController {
/// parts of the traversal. /// parts of the traversal.
/// * [orderedEquals], which can be given an [Iterable<Matcher>] to exactly /// * [orderedEquals], which can be given an [Iterable<Matcher>] to exactly
/// match the order of the traversal. /// match the order of the traversal.
Iterable<SemanticsNode> simulatedAccessibilityTraversal({Finder? start, Finder? end}) { Iterable<SemanticsNode> simulatedAccessibilityTraversal({Finder? start, Finder? end, FlutterView? view}) {
TestAsyncUtils.guardSync(); TestAsyncUtils.guardSync();
FlutterView? startView;
FlutterView? endView;
if (start != null) {
startView = _controller.viewOf(start);
if (view != null && startView != view) {
throw StateError(
'The start node is not part of the provided view.\n'
'Finder: ${start.description}\n'
'View of start node: $startView\n'
'Specified view: $view'
);
}
}
if (end != null) {
endView = _controller.viewOf(end);
if (view != null && endView != view) {
throw StateError(
'The end node is not part of the provided view.\n'
'Finder: ${end.description}\n'
'View of end node: $endView\n'
'Specified view: $view'
);
}
}
if (endView != null && startView != null && endView != startView) {
throw StateError(
'The start and end node are in different views.\n'
'Start finder: ${start!.description}\n'
'End finder: ${end!.description}\n'
'View of start node: $startView\n'
'View of end node: $endView'
);
}
final FlutterView actualView = view ?? startView ?? endView ?? _controller.view;
final RenderView renderView = _controller.binding.renderViews.firstWhere((RenderView r) => r.flutterView == actualView);
final List<SemanticsNode> traversal = <SemanticsNode>[]; final List<SemanticsNode> traversal = <SemanticsNode>[];
_traverse(_binding.pipelineOwner.semanticsOwner!.rootSemanticsNode!, traversal); _traverse(renderView.owner!.semanticsOwner!.rootSemanticsNode!, traversal);
int startIndex = 0; int startIndex = 0;
int endIndex = traversal.length - 1; int endIndex = traversal.length - 1;
@ -229,8 +273,7 @@ class SemanticsController {
/// Concrete subclasses must implement the [pump] method. /// Concrete subclasses must implement the [pump] method.
abstract class WidgetController { abstract class WidgetController {
/// Creates a widget controller that uses the given binding. /// Creates a widget controller that uses the given binding.
WidgetController(this.binding) WidgetController(this.binding);
: _semantics = SemanticsController._(binding);
/// A reference to the current instance of the binding. /// A reference to the current instance of the binding.
final WidgetsBinding binding; final WidgetsBinding binding;
@ -280,7 +323,7 @@ abstract class WidgetController {
return _semantics; return _semantics;
} }
final SemanticsController _semantics; late final SemanticsController _semantics = SemanticsController._(this);
// FINDER API // FINDER API
@ -297,14 +340,16 @@ abstract class WidgetController {
/// * [view] which returns the [TestFlutterView] used when only a single /// * [view] which returns the [TestFlutterView] used when only a single
/// view is being used. /// view is being used.
TestFlutterView viewOf(Finder finder) { TestFlutterView viewOf(Finder finder) {
final View view = firstWidget<View>( return _viewOf(finder) as TestFlutterView;
}
FlutterView _viewOf(Finder finder) {
return firstWidget<View>(
find.ancestor( find.ancestor(
of: finder, of: finder,
matching: find.byType(View), matching: find.byType(View),
) ),
); ).view;
return view.view as TestFlutterView;
} }
/// Checks if `finder` exists in the tree. /// Checks if `finder` exists in the tree.
@ -516,7 +561,12 @@ abstract class WidgetController {
} }
/// Returns a list of all the [Layer] objects in the rendering. /// Returns a list of all the [Layer] objects in the rendering.
List<Layer> get layers => _walkLayers(binding.renderView.debugLayer!).toList(); List<Layer> get layers {
return <Layer>[
for (final RenderView renderView in binding.renderViews)
..._walkLayers(renderView.debugLayer!)
];
}
Iterable<Layer> _walkLayers(Layer layer) sync* { Iterable<Layer> _walkLayers(Layer layer) sync* {
TestAsyncUtils.guardSync(); TestAsyncUtils.guardSync();
yield layer; yield layer;
@ -1190,10 +1240,10 @@ abstract class WidgetController {
} }
/// Forwards the given location to the binding's hitTest logic. /// Forwards the given location to the binding's hitTest logic.
HitTestResult hitTestOnBinding(Offset location) { HitTestResult hitTestOnBinding(Offset location, { int? viewId }) {
viewId ??= view.viewId;
final HitTestResult result = HitTestResult(); final HitTestResult result = HitTestResult();
// TODO(goderbauer): Support multiple views in flutter_test pointer event handling, https://github.com/flutter/flutter/issues/128281 binding.hitTestInView(result, location, viewId);
binding.hitTest(result, location); // ignore: deprecated_member_use
return result; return result;
} }
@ -1313,9 +1363,9 @@ abstract class WidgetController {
final RenderBox box = element.renderObject! as RenderBox; final RenderBox box = element.renderObject! as RenderBox;
final Offset location = box.localToGlobal(sizeToPoint(box.size)); final Offset location = box.localToGlobal(sizeToPoint(box.size));
if (warnIfMissed) { if (warnIfMissed) {
final FlutterView view = _viewOf(finder);
final HitTestResult result = HitTestResult(); final HitTestResult result = HitTestResult();
// TODO(goderbauer): Support multiple views in flutter_test pointer event handling, https://github.com/flutter/flutter/issues/128281 binding.hitTestInView(result, location, view.viewId);
binding.hitTest(result, location); // ignore: deprecated_member_use
bool found = false; bool found = false;
for (final HitTestEntry entry in result.path) { for (final HitTestEntry entry in result.path) {
if (entry.target == box) { if (entry.target == box) {
@ -1324,15 +1374,16 @@ abstract class WidgetController {
} }
} }
if (!found) { if (!found) {
final RenderView renderView = binding.renderViews.firstWhere((RenderView r) => r.flutterView == view);
bool outOfBounds = false; bool outOfBounds = false;
outOfBounds = !(Offset.zero & binding.renderView.size).contains(location); outOfBounds = !(Offset.zero & renderView.size).contains(location);
if (hitTestWarningShouldBeFatal) { if (hitTestWarningShouldBeFatal) {
throw FlutterError.fromParts(<DiagnosticsNode>[ throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Finder specifies a widget that would not receive pointer events.'), ErrorSummary('Finder specifies a widget that would not receive pointer events.'),
ErrorDescription('A call to $callee() with finder "$finder" derived an Offset ($location) that would not hit test on the specified widget.'), ErrorDescription('A call to $callee() with finder "$finder" derived an Offset ($location) that would not hit test on the specified widget.'),
ErrorHint('Maybe the widget is actually off-screen, or another widget is obscuring it, or the widget cannot receive pointer events.'), ErrorHint('Maybe the widget is actually off-screen, or another widget is obscuring it, or the widget cannot receive pointer events.'),
if (outOfBounds) if (outOfBounds)
ErrorHint('Indeed, $location is outside the bounds of the root of the render tree, ${binding.renderView.size}.'), ErrorHint('Indeed, $location is outside the bounds of the root of the render tree, ${renderView.size}.'),
box.toDiagnosticsNode(name: 'The finder corresponds to this RenderBox', style: DiagnosticsTreeStyle.singleLine), box.toDiagnosticsNode(name: 'The finder corresponds to this RenderBox', style: DiagnosticsTreeStyle.singleLine),
ErrorDescription('The hit test result at that offset is: $result'), ErrorDescription('The hit test result at that offset is: $result'),
ErrorDescription('If you expected this target not to be able to receive pointer events, pass "warnIfMissed: false" to "$callee()".'), ErrorDescription('If you expected this target not to be able to receive pointer events, pass "warnIfMissed: false" to "$callee()".'),
@ -1343,7 +1394,7 @@ abstract class WidgetController {
'\n' '\n'
'Warning: A call to $callee() with finder "$finder" derived an Offset ($location) that would not hit test on the specified widget.\n' 'Warning: A call to $callee() with finder "$finder" derived an Offset ($location) that would not hit test on the specified widget.\n'
'Maybe the widget is actually off-screen, or another widget is obscuring it, or the widget cannot receive pointer events.\n' 'Maybe the widget is actually off-screen, or another widget is obscuring it, or the widget cannot receive pointer events.\n'
'${outOfBounds ? "Indeed, $location is outside the bounds of the root of the render tree, ${binding.renderView.size}.\n" : ""}' '${outOfBounds ? "Indeed, $location is outside the bounds of the root of the render tree, ${renderView.size}.\n" : ""}'
'The finder corresponds to this RenderBox: $box\n' 'The finder corresponds to this RenderBox: $box\n'
'The hit test result at that offset is: $result\n' 'The hit test result at that offset is: $result\n'
'${StackTrace.current}' '${StackTrace.current}'

View File

@ -639,11 +639,11 @@ class _HitTestableFinder extends ChainedFinder {
@override @override
Iterable<Element> filter(Iterable<Element> parentCandidates) sync* { Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
for (final Element candidate in parentCandidates) { for (final Element candidate in parentCandidates) {
final int viewId = candidate.findAncestorWidgetOfExactType<View>()!.view.viewId;
final RenderBox box = candidate.renderObject! as RenderBox; final RenderBox box = candidate.renderObject! as RenderBox;
final Offset absoluteOffset = box.localToGlobal(alignment.alongSize(box.size)); final Offset absoluteOffset = box.localToGlobal(alignment.alongSize(box.size));
final HitTestResult hitResult = HitTestResult(); final HitTestResult hitResult = HitTestResult();
// TODO(goderbauer): Support multiple views in flutter_test pointer event handling, https://github.com/flutter/flutter/issues/128281 WidgetsBinding.instance.hitTestInView(hitResult, absoluteOffset, viewId);
WidgetsBinding.instance.hitTest(hitResult, absoluteOffset); // ignore: deprecated_member_use
for (final HitTestEntry entry in hitResult.path) { for (final HitTestEntry entry in hitResult.path) {
if (entry.target == candidate.renderObject) { if (entry.target == candidate.renderObject) {
yield candidate; yield candidate;

View File

@ -569,22 +569,10 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
EnginePhase phase = EnginePhase.sendSemanticsUpdate, EnginePhase phase = EnginePhase.sendSemanticsUpdate,
]) { ]) {
return TestAsyncUtils.guard<void>(() { return TestAsyncUtils.guard<void>(() {
return _pumpWidget( binding.attachRootWidget(binding.wrapWithDefaultView(widget));
binding.wrapWithDefaultView(widget),
duration,
phase,
);
});
}
Future<void> _pumpWidget(
Widget widget, [
Duration? duration,
EnginePhase phase = EnginePhase.sendSemanticsUpdate,
]) {
binding.attachRootWidget(widget);
binding.scheduleFrame(); binding.scheduleFrame();
return binding.pump(duration, phase); return binding.pump(duration, phase);
});
} }
@override @override
@ -745,12 +733,14 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
'your widget tree in a RootRestorationScope?', 'your widget tree in a RootRestorationScope?',
); );
return TestAsyncUtils.guard<void>(() async { return TestAsyncUtils.guard<void>(() async {
final Widget widget = ((binding.rootElement! as RenderObjectToWidgetElement<RenderObject>).widget as RenderObjectToWidgetAdapter<RenderObject>).child!; final RootWidget widget = binding.rootElement!.widget as RootWidget;
final TestRestorationData restorationData = binding.restorationManager.restorationData; final TestRestorationData restorationData = binding.restorationManager.restorationData;
runApp(Container(key: UniqueKey())); runApp(Container(key: UniqueKey()));
await pump(); await pump();
binding.restorationManager.restoreFrom(restorationData); binding.restorationManager.restoreFrom(restorationData);
return _pumpWidget(widget); binding.attachToBuildOwner(widget);
binding.scheduleFrame();
return binding.pump();
}); });
} }
@ -837,9 +827,11 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
bool get hasRunningAnimations => binding.transientCallbackCount > 0; bool get hasRunningAnimations => binding.transientCallbackCount > 0;
@override @override
HitTestResult hitTestOnBinding(Offset location) { HitTestResult hitTestOnBinding(Offset location, {int? viewId}) {
location = binding.localToGlobal(location, binding.renderView); viewId ??= view.viewId;
return super.hitTestOnBinding(location); final RenderView renderView = binding.renderViews.firstWhere((RenderView r) => r.flutterView.viewId == viewId);
location = binding.localToGlobal(location, renderView);
return super.hitTestOnBinding(location, viewId: viewId);
} }
@override @override
@ -861,7 +853,9 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
.map((HitTestEntry candidate) => candidate.target) .map((HitTestEntry candidate) => candidate.target)
.whereType<RenderObject>() .whereType<RenderObject>()
.first; .first;
final Element? innerTargetElement = _lastWhereOrNull( final Element? innerTargetElement = binding.renderViews.contains(innerTarget)
? null
: _lastWhereOrNull(
collectAllElementsFrom(binding.rootElement!, skipOffstage: true), collectAllElementsFrom(binding.rootElement!, skipOffstage: true),
(Element element) => element.renderObject == innerTarget, (Element element) => element.renderObject == innerTarget,
); );
@ -1060,6 +1054,8 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
int? _lastRecordedSemanticsHandles; int? _lastRecordedSemanticsHandles;
// TODO(goderbauer): Only use binding.debugOutstandingSemanticsHandles when deprecated binding.pipelineOwner is removed.
// ignore: deprecated_member_use
int get _currentSemanticsHandles => binding.debugOutstandingSemanticsHandles + binding.pipelineOwner.debugOutstandingSemanticsHandles; int get _currentSemanticsHandles => binding.debugOutstandingSemanticsHandles + binding.pipelineOwner.debugOutstandingSemanticsHandles;
void _recordNumberOfSemanticsHandles() { void _recordNumberOfSemanticsHandles() {

View File

@ -0,0 +1,135 @@
// 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:convert';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Detects tap targets in all views', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await pumpViews(
tester: tester,
viewContents: <Widget>[
SizedBox(
width: 47.0,
height: 47.0,
child: GestureDetector(onTap: () {}),
),
SizedBox(
width: 46.0,
height: 46.0,
child: GestureDetector(onTap: () {}),
),
],
);
final Evaluation result = await androidTapTargetGuideline.evaluate(tester);
expect(result.passed, false);
expect(
result.reason,
contains('expected tap target size of at least Size(48.0, 48.0), but found Size(47.0, 47.0)'),
);
expect(
result.reason,
contains('expected tap target size of at least Size(48.0, 48.0), but found Size(46.0, 46.0)'),
);
handle.dispose();
});
testWidgets('Detects labeled tap targets in all views', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await pumpViews(
tester: tester,
viewContents: <Widget>[
SizedBox(
width: 47.0,
height: 47.0,
child: GestureDetector(onTap: () {}),
),
SizedBox(
width: 46.0,
height: 46.0,
child: GestureDetector(onTap: () {}),
),
],
);
final Evaluation result = await labeledTapTargetGuideline.evaluate(tester);
expect(result.passed, false);
final List<String> lines = const LineSplitter().convert(result.reason!);
expect(lines, hasLength(2));
expect(lines.first, startsWith('SemanticsNode#1(Rect.fromLTRB(0.0, 0.0, 47.0, 47.0)'));
expect(lines.first, endsWith('expected tappable node to have semantic label, but none was found.'));
expect(lines.last, startsWith('SemanticsNode#2(Rect.fromLTRB(0.0, 0.0, 46.0, 46.0)'));
expect(lines.last, endsWith('expected tappable node to have semantic label, but none was found.'));
handle.dispose();
});
testWidgets('Detects contrast problems in all views', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await pumpViews(
tester: tester,
viewContents: <Widget>[
Container(
width: 200.0,
height: 200.0,
color: Colors.yellow,
child: const Text(
'this is a test',
style: TextStyle(fontSize: 14.0, color: Colors.yellowAccent),
),
),
Container(
width: 200.0,
height: 200.0,
color: Colors.yellow,
child: const Text(
'this is a test',
style: TextStyle(fontSize: 25.0, color: Colors.yellowAccent),
),
),
],
);
final Evaluation result = await textContrastGuideline.evaluate(tester);
expect(result.passed, false);
expect(result.reason, contains('Expected contrast ratio of at least 4.5 but found 0.88 for a font size of 14.0.'));
expect(result.reason, contains('Expected contrast ratio of at least 3.0 but found 0.88 for a font size of 25.0.'));
handle.dispose();
});
}
Future<void> pumpViews({required WidgetTester tester, required List<Widget> viewContents}) {
final List<Widget> views = <Widget>[
for (int i = 0; i < viewContents.length; i++)
View(
view: FakeView(tester.view, viewId: i + 100),
child: Center(
child: viewContents[i],
),
),
];
tester.binding.attachRootWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ViewCollection(
views: views,
),
),
);
tester.binding.scheduleFrame();
return tester.binding.pump();
}
class FakeView extends TestFlutterView{
FakeView(FlutterView view, { this.viewId = 100 }) : super(
view: view,
platformDispatcher: view.platformDispatcher as TestPlatformDispatcher,
display: view.display as TestDisplay,
);
@override
final int viewId;
}

View File

@ -0,0 +1,232 @@
// 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';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('simulatedAccessibilityTraversal - start and end in same view', (WidgetTester tester) async {
await pumpViews(tester: tester);
expect(
tester.semantics.simulatedAccessibilityTraversal(
start: find.text('View2Child1'),
end: find.text('View2Child3'),
).map((SemanticsNode node) => node.label),
<String>[
'View2Child1',
'View2Child2',
'View2Child3',
],
);
});
testWidgets('simulatedAccessibilityTraversal - start not specified', (WidgetTester tester) async {
await pumpViews(tester: tester);
expect(
tester.semantics.simulatedAccessibilityTraversal(
end: find.text('View2Child3'),
).map((SemanticsNode node) => node.label),
<String>[
'View2Child0',
'View2Child1',
'View2Child2',
'View2Child3',
],
);
});
testWidgets('simulatedAccessibilityTraversal - end not specified', (WidgetTester tester) async {
await pumpViews(tester: tester);
expect(
tester.semantics.simulatedAccessibilityTraversal(
start: find.text('View2Child1'),
).map((SemanticsNode node) => node.label),
<String>[
'View2Child1',
'View2Child2',
'View2Child3',
'View2Child4',
],
);
});
testWidgets('simulatedAccessibilityTraversal - nothing specified', (WidgetTester tester) async {
await pumpViews(tester: tester);
expect(
tester.semantics.simulatedAccessibilityTraversal().map((SemanticsNode node) => node.label),
<String>[
'View1Child0',
'View1Child1',
'View1Child2',
'View1Child3',
'View1Child4',
],
);
// Should be traversing over tester.view.
expect(tester.viewOf(find.text('View1Child0')), tester.view);
});
testWidgets('simulatedAccessibilityTraversal - only view specified', (WidgetTester tester) async {
await pumpViews(tester: tester);
expect(
tester.semantics.simulatedAccessibilityTraversal(
view: tester.viewOf(find.text('View2Child1')),
).map((SemanticsNode node) => node.label),
<String>[
'View2Child0',
'View2Child1',
'View2Child2',
'View2Child3',
'View2Child4',
],
);
});
testWidgets('simulatedAccessibilityTraversal - everything specified', (WidgetTester tester) async {
await pumpViews(tester: tester);
expect(
tester.semantics.simulatedAccessibilityTraversal(
start: find.text('View2Child1'),
end: find.text('View2Child3'),
view: tester.viewOf(find.text('View2Child1')),
).map((SemanticsNode node) => node.label),
<String>[
'View2Child1',
'View2Child2',
'View2Child3',
],
);
});
testWidgets('simulatedAccessibilityTraversal - start and end not in same view', (WidgetTester tester) async {
await pumpViews(tester: tester);
expect(
() => tester.semantics.simulatedAccessibilityTraversal(
start: find.text('View2Child1'),
end: find.text('View1Child3'),
),
throwsA(isStateError.having(
(StateError e) => e.message,
'message',
contains('The start and end node are in different views.'),
)),
);
});
testWidgets('simulatedAccessibilityTraversal - start is not in view', (WidgetTester tester) async {
await pumpViews(tester: tester);
expect(
() => tester.semantics.simulatedAccessibilityTraversal(
start: find.text('View2Child1'),
end: find.text('View1Child3'),
view: tester.viewOf(find.text('View1Child3')),
),
throwsA(isStateError.having(
(StateError e) => e.message,
'message',
contains('The start node is not part of the provided view.'),
)),
);
});
testWidgets('simulatedAccessibilityTraversal - end is not in view', (WidgetTester tester) async {
await pumpViews(tester: tester);
expect(
() => tester.semantics.simulatedAccessibilityTraversal(
start: find.text('View2Child1'),
end: find.text('View1Child3'),
view: tester.viewOf(find.text('View2Child1')),
),
throwsA(isStateError.having(
(StateError e) => e.message,
'message',
contains('The end node is not part of the provided view.'),
)),
);
});
testWidgets('viewOf', (WidgetTester tester) async {
await pumpViews(tester: tester);
expect(tester.viewOf(find.text('View0Child0')).viewId, 100);
expect(tester.viewOf(find.text('View1Child1')).viewId, tester.view.viewId);
expect(tester.viewOf(find.text('View2Child2')).viewId, 102);
});
testWidgets('layers includes layers from all views', (WidgetTester tester) async {
await pumpViews(tester: tester);
const int numberOfViews = 3;
expect(tester.binding.renderViews.length, numberOfViews); // One RenderView for each FlutterView.
final List<Layer> layers = tester.layers;
// Each RenderView contributes a TransformLayer and a PictureLayer.
expect(layers, hasLength(numberOfViews * 2));
expect(layers.whereType<TransformLayer>(), hasLength(numberOfViews));
expect(layers.whereType<PictureLayer>(), hasLength(numberOfViews));
expect(
layers.whereType<TransformLayer>().map((TransformLayer l ) => l.owner),
containsAll(tester.binding.renderViews),
);
});
testWidgets('hitTestOnBinding', (WidgetTester tester) async {
await pumpViews(tester: tester);
// Not specifying a viewId hit tests on tester.view:
HitTestResult result = tester.hitTestOnBinding(Offset.zero);
expect(result.path.map((HitTestEntry h) => h.target).whereType<RenderView>().single.flutterView, tester.view);
// Specifying a viewId is respected:
result = tester.hitTestOnBinding(Offset.zero, viewId: 100);
expect(result.path.map((HitTestEntry h) => h.target).whereType<RenderView>().single.flutterView.viewId, 100);
result = tester.hitTestOnBinding(Offset.zero, viewId: 102);
expect(result.path.map((HitTestEntry h) => h.target).whereType<RenderView>().single.flutterView.viewId, 102);
});
testWidgets('hitTestable works in different Views', (WidgetTester tester) async {
await pumpViews(tester: tester);
expect((find.text('View0Child0').hitTestable().evaluate().single.widget as Text).data, 'View0Child0');
expect((find.text('View1Child1').hitTestable().evaluate().single.widget as Text).data, 'View1Child1');
expect((find.text('View2Child2').hitTestable().evaluate().single.widget as Text).data, 'View2Child2');
});
}
Future<void> pumpViews({required WidgetTester tester}) {
final List<Widget> views = <Widget>[
for (int i = 0; i < 3; i++)
View(
view: i == 1 ? tester.view : FakeView(tester.view, viewId: i + 100),
child: Center(
child: Column(
children: <Widget>[
for (int c = 0; c < 5; c++)
Semantics(container: true, child: Text('View${i}Child$c')),
],
),
),
),
];
tester.binding.attachRootWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ViewCollection(
views: views,
),
),
);
tester.binding.scheduleFrame();
return tester.binding.pump();
}
class FakeView extends TestFlutterView{
FakeView(FlutterView view, { this.viewId = 100 }) : super(
view: view,
platformDispatcher: view.platformDispatcher as TestPlatformDispatcher,
display: view.display as TestDisplay,
);
@override
final int viewId;
}

View File

@ -22,9 +22,9 @@ Future<void> testExecutable(FutureOr<void> Function() testMain) async {
void pipelineOwnerTestRun() { void pipelineOwnerTestRun() {
testWidgets('open SemanticsHandle from PipelineOwner fails test', (WidgetTester tester) async { testWidgets('open SemanticsHandle from PipelineOwner fails test', (WidgetTester tester) async {
final int outstandingHandles = tester.binding.pipelineOwner.debugOutstandingSemanticsHandles; final int outstandingHandles = tester.binding.debugOutstandingSemanticsHandles;
tester.binding.pipelineOwner.ensureSemantics(); tester.binding.ensureSemantics();
expect(tester.binding.pipelineOwner.debugOutstandingSemanticsHandles, outstandingHandles + 1); expect(tester.binding.debugOutstandingSemanticsHandles, outstandingHandles + 1);
// SemanticsHandle is not disposed on purpose to verify in tearDown that // SemanticsHandle is not disposed on purpose to verify in tearDown that
// the test failed due to an active SemanticsHandle. // the test failed due to an active SemanticsHandle.
}); });

View File

@ -4,6 +4,7 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
// Only check the initial lines of the message, since the message walks the // Only check the initial lines of the message, since the message walks the
@ -82,7 +83,7 @@ No widgets found at Offset(1.0, 1.0).
), ),
); );
final Size originalSize = tester.binding.createViewConfiguration().size; final Size originalSize = tester.binding.createViewConfigurationFor(tester.binding.renderView).size; // ignore: deprecated_member_use
await tester.binding.setSurfaceSize(const Size(2000, 1800)); await tester.binding.setSurfaceSize(const Size(2000, 1800));
try { try {
await tester.pump(); await tester.pump();
@ -126,6 +127,7 @@ class _MockLiveTestWidgetsFlutterBinding extends LiveTestWidgetsFlutterBinding {
// real devices touches sends event in the global coordinate system. // real devices touches sends event in the global coordinate system.
// See the documentation of [handlePointerEventForSource] for details. // See the documentation of [handlePointerEventForSource] for details.
if (source == TestBindingEventSource.test) { if (source == TestBindingEventSource.test) {
final RenderView renderView = renderViews.firstWhere((RenderView r) => r.flutterView.viewId == event.viewId);
final PointerEvent globalEvent = event.copyWith(position: localToGlobal(event.position, renderView)); final PointerEvent globalEvent = event.copyWith(position: localToGlobal(event.position, renderView));
return super.handlePointerEventForSource(globalEvent); return super.handlePointerEventForSource(globalEvent);
} }

View File

@ -100,10 +100,6 @@ https://flutter.dev/docs/testing/integration-tests#testing-on-firebase-test-lab
// under debug mode. // under debug mode.
static bool _firstRun = false; static bool _firstRun = false;
/// Artificially changes the surface size to `size` on the Widget binding,
/// then flushes microtasks.
///
/// Set to null to use the default surface size.
@override @override
Future<void> setSurfaceSize(Size? size) { Future<void> setSurfaceSize(Size? size) {
return TestAsyncUtils.guard<void>(() async { return TestAsyncUtils.guard<void>(() async {
@ -117,12 +113,11 @@ https://flutter.dev/docs/testing/integration-tests#testing-on-firebase-test-lab
} }
@override @override
ViewConfiguration createViewConfiguration() { ViewConfiguration createViewConfigurationFor(RenderView renderView) {
final FlutterView view = platformDispatcher.implicitView!; final FlutterView view = renderView.flutterView;
final double devicePixelRatio = view.devicePixelRatio; final Size? surfaceSize = view == platformDispatcher.implicitView ? _surfaceSize : null;
final Size size = _surfaceSize ?? view.physicalSize / devicePixelRatio;
return TestViewConfiguration.fromView( return TestViewConfiguration.fromView(
size: size, size: surfaceSize ?? view.physicalSize / view.devicePixelRatio,
view: view, view: view,
); );
} }
@ -442,11 +437,11 @@ https://flutter.dev/docs/testing/integration-tests#testing-on-firebase-test-lab
Timeout defaultTestTimeout = Timeout.none; Timeout defaultTestTimeout = Timeout.none;
@override @override
void attachRootWidget(Widget rootWidget) { Widget wrapWithDefaultView(Widget rootWidget) {
// This is a workaround where screenshots of root widgets have incorrect // This is a workaround where screenshots of root widgets have incorrect
// bounds. // bounds.
// TODO(jiahaog): Remove when https://github.com/flutter/flutter/issues/66006 is fixed. // TODO(jiahaog): Remove when https://github.com/flutter/flutter/issues/66006 is fixed.
super.attachRootWidget(RepaintBoundary(child: rootWidget)); return super.wrapWithDefaultView(RepaintBoundary(child: rootWidget));
} }
@override @override