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),
view: tester.view,
);
final RenderView renderView = WidgetsBinding.instance.renderView;
final RenderView renderView = WidgetsBinding.instance.renderViews.single;
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.benchmark;
watch.start();

View File

@ -1361,7 +1361,7 @@ Future<void> _runWebTreeshakeTest() async {
final String javaScript = mainDartJs.readAsStringSync();
// 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';
int count = 0;

View File

@ -79,8 +79,8 @@ Future<void> smokeDemo(WidgetTester tester, GalleryDemo demo) async {
// Verify that the dumps are pretty.
final String routeName = demo.routeName;
verifyToStringOutput('debugDumpApp', routeName, WidgetsBinding.instance.rootElement!.toStringDeep());
verifyToStringOutput('debugDumpRenderTree', routeName, RendererBinding.instance.renderView.toStringDeep());
verifyToStringOutput('debugDumpLayerTree', routeName, RendererBinding.instance.renderView.debugLayer?.toStringDeep() ?? '');
verifyToStringOutput('debugDumpRenderTree', routeName, RendererBinding.instance.renderViews.single.toStringDeep());
verifyToStringOutput('debugDumpLayerTree', routeName, RendererBinding.instance.renderViews.single.debugLayer?.toStringDeep() ?? '');
verifyToStringOutput('debugDumpFocusTree', routeName, WidgetsBinding.instance.focusManager.toStringDeep());
// 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.
import 'package:flutter/rendering.dart';
import 'src/binding.dart';
import 'src/sector_layout.dart';
RenderBox buildSectorExample() {
@ -21,5 +22,5 @@ RenderBox buildSectorExample() {
}
void main() {
RenderingFlutterBinding(root: buildSectorExample()).scheduleFrame();
ViewRenderingFlutterBinding(root: buildSectorExample()).scheduleFrame();
}

View File

@ -7,6 +7,7 @@
import 'package:flutter/rendering.dart';
import 'src/binding.dart';
import 'src/solid_color_box.dart';
void main() {
@ -86,5 +87,5 @@ void main() {
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 'src/binding.dart';
void main() {
// We use RenderingFlutterBinding to attach the render tree to the window.
RenderingFlutterBinding(
// We use ViewRenderingFlutterBinding to attach the render tree to the window.
ViewRenderingFlutterBinding(
// The root of our render tree is a RenderPositionedBox, which centers its
// child both vertically and horizontally.
root: RenderPositionedBox(

View File

@ -11,6 +11,8 @@ import 'package:flutter/animation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'src/binding.dart';
class NonStopVSync implements TickerProvider {
const NonStopVSync();
@override
@ -42,7 +44,7 @@ void main() {
child: spin,
);
// 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
// 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/rendering.dart';
import 'src/binding.dart';
// Material design colors. :p
List<Color> _kColors = <Color>[
Colors.teal,
@ -133,5 +135,5 @@ void main() {
..left = 20.0;
// 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,
children: <Widget>[
ElevatedButton(
child: Row(
child: const Row(
children: <Widget>[
Image.network('https://flutter.dev/images/favicon.png'),
const Text('PRESS ME'),
FlutterLogo(),
Text('PRESS ME'),
],
),
onPressed: () {
@ -102,6 +102,16 @@ void main() {
transformBox = RenderTransform(child: flexRoot, transform: Matrix4.identity(), alignment: Alignment.center);
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);
}

View File

@ -10,7 +10,6 @@ import 'package:flutter/scheduler.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter/services.dart';
import 'box.dart';
import 'debug.dart';
import 'mouse_tracker.dart';
import 'object.dart';
@ -22,28 +21,34 @@ export 'package:flutter/gestures.dart' show HitTestResult;
// Examples can assume:
// 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 {
@override
void initInstances() {
super.initInstances();
_instance = this;
_pipelineOwner = PipelineOwner(
onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,
onSemanticsUpdate: _handleSemanticsUpdate,
onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,
);
_rootPipelineOwner = createRootPipelineOwner();
platformDispatcher
..onMetricsChanged = handleMetricsChanged
..onTextScaleFactorChanged = handleTextScaleFactorChanged
..onPlatformBrightnessChanged = handlePlatformBrightnessChanged;
initRenderView();
addPersistentFrameCallback(_handlePersistentFrameCallback);
initMouseTracker();
if (kIsWeb) {
addPostFrameCallback(_handleWebFirstFrame);
}
_pipelineOwner.attach(_manifold);
rootPipelineOwner.attach(_manifold);
}
/// The current [RendererBinding], if one has been created.
@ -108,9 +113,8 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
registerServiceExtension(
name: RenderingServiceExtensions.debugDumpLayerTree.name,
callback: (Map<String, String> parameters) async {
final String data = RendererBinding.instance.renderView.debugLayer?.toStringDeep() ?? 'Layer tree unavailable.';
return <String, Object>{
'data': data,
'data': _debugCollectLayerTrees(),
};
},
);
@ -155,9 +159,8 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
registerServiceExtension(
name: RenderingServiceExtensions.debugDumpRenderTree.name,
callback: (Map<String, String> parameters) async {
final String data = RendererBinding.instance.renderView.toStringDeep();
return <String, Object>{
'data': data,
'data': _debugCollectRenderTrees(),
};
},
);
@ -165,7 +168,7 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
name: RenderingServiceExtensions.debugDumpSemanticsTreeInTraversalOrder.name,
callback: (Map<String, String> parameters) async {
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,
callback: (Map<String, String> parameters) async {
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);
/// 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
/// notification.
MouseTracker get mouseTracker => _mouseTracker!;
MouseTracker? _mouseTracker;
/// The render tree's owner, which maintains dirty state for layout,
/// composite, paint, and accessibility semantics.
PipelineOwner get pipelineOwner => _pipelineOwner;
late PipelineOwner _pipelineOwner;
/// Deprecated. Will be removed in a future version of Flutter.
///
/// This is typically the owner of the render tree bootstrapped by [runApp]
/// 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.
RenderView get renderView => _pipelineOwner.rootNode! as RenderView;
/// Sets the given [RenderView] object (which must not be null), and its tree, to
/// be the new render tree to display. The previous tree, if any, is detached.
set renderView(RenderView value) {
_pipelineOwner.rootNode = value;
/// Deprecated. Will be removed in a future version of Flutter.
///
/// This is typically the root of the render tree bootstrapped by [runApp].
///
/// However, by default this render view is not associated with any
/// [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.
@ -240,8 +361,12 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
@protected
@visibleForTesting
void handleMetricsChanged() {
renderView.configuration = createViewConfiguration();
if (renderView.child != null) {
bool forceFrame = false;
for (final RenderView view in renderViews) {
forceFrame = forceFrame || view.child != null;
view.configuration = createViewConfigurationFor(view);
}
if (forceFrame) {
scheduleForcedFrame();
}
}
@ -288,25 +413,6 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
@protected
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
/// mice, for hover notification.
///
@ -335,19 +441,10 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
@override
void performSemanticsAction(SemanticsActionEvent action) {
_pipelineOwner.semanticsOwner?.performAction(action.nodeId, action.type, action.arguments);
}
void _handleSemanticsOwnerCreated() {
renderView.scheduleInitialSemantics();
}
void _handleSemanticsUpdate(ui.SemanticsUpdate update) {
renderView.updateSemantics(update);
}
void _handleSemanticsOwnerDisposed() {
renderView.clearSemantics();
// 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.
_viewIdToRenderView[action.viewId]?.owner?.semanticsOwner?.performAction(action.nodeId, action.type, action.arguments);
}
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.
@protected
void drawFrame() {
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
rootPipelineOwner.flushLayout();
rootPipelineOwner.flushCompositingBits();
rootPipelineOwner.flushPaint();
if (sendFramesToEngine) {
for (final RenderView renderView in renderViews) {
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;
}
}
@ -509,7 +608,9 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
FlutterTimeline.startSync('Preparing Hot Reload (layout)');
}
try {
for (final RenderView renderView in renderViews) {
renderView.reassemble();
}
} finally {
if (!kReleaseMode) {
FlutterTimeline.finishSync();
@ -520,18 +621,9 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
await endOfFrame;
}
late final int _implicitViewId = platformDispatcher.implicitView!.viewId;
@override
void hitTestInView(HitTestResult result, Offset position, int viewId) {
// Currently Flutter only supports one view, the implicit view `renderView`.
// 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);
_viewIdToRenderView[viewId]?.hitTest(result, position: position);
super.hitTestInView(result, position, viewId);
}
@ -541,40 +633,93 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
child.markNeedsPaint();
child.visitChildren(visitor);
};
instance.renderView.visitChildren(visitor);
for (final RenderView renderView in renderViews) {
renderView.visitChildren(visitor);
}
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() {
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() {
debugPrint(RendererBinding.instance.renderView.debugLayer?.toStringDeep());
debugPrint(_debugCollectLayerTrees());
}
/// Prints a textual representation of the entire semantics tree.
/// This will only work if there is a semantics client attached.
/// Otherwise, a notice that no semantics are available will be printed.
String _debugCollectSemanticsTrees(DebugSemanticsDumpOrder childOrder) {
if (RendererBinding.instance.renderViews.isEmpty) {
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
/// controlled by the [childOrder] parameter.
void debugDumpSemanticsTree([DebugSemanticsDumpOrder childOrder = DebugSemanticsDumpOrder.traversalOrder]) {
debugPrint(_generateSemanticsTree(childOrder));
debugPrint(_debugCollectSemanticsTrees(childOrder));
}
String _generateSemanticsTree(DebugSemanticsDumpOrder childOrder) {
final String? tree = RendererBinding.instance.renderView.debugSemantics?.toStringDeep(childOrder: childOrder);
if (tree != null) {
return tree;
}
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.';
/// Prints a textual representation of the [PipelineOwner] tree rooted at
/// [RendererBinding.rootPipelineOwner].
void debugDumpPipelineOwnerTree() {
debugPrint(RendererBinding.instance.rootPipelineOwner.toStringDeep());
}
/// 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
/// library, such as the Flutter Widgets library, then you would use
/// 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 {
/// 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
/// [RendererBinding]. If no binding has yet been initialized, the
/// [RenderingFlutterBinding] class is used to create and initialize
@ -645,3 +789,82 @@ class _BindingPipelineManifold extends ChangeNotifier implements PipelineManifol
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
/// in a given tree must be attached to the same [PipelineManifold]. This
/// happens automatically during [adoptChild].
class PipelineOwner {
class PipelineOwner with DiagnosticableTreeMixin {
/// Creates a pipeline owner.
///
/// Typically created by the binding (e.g., [RendererBinding]), but can be
@ -984,7 +984,7 @@ class PipelineOwner {
return true;
}());
FlutterTimeline.startSync(
'LAYOUT',
'LAYOUT$_debugRootSuffixForTimelineEventNames',
arguments: debugTimelineArguments,
);
}
@ -1071,7 +1071,7 @@ class PipelineOwner {
/// [flushPaint].
void flushCompositingBits() {
if (!kReleaseMode) {
FlutterTimeline.startSync('UPDATING COMPOSITING BITS');
FlutterTimeline.startSync('UPDATING COMPOSITING BITS$_debugRootSuffixForTimelineEventNames');
}
_nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
for (final RenderObject node in _nodesNeedingCompositingBitsUpdate) {
@ -1120,7 +1120,7 @@ class PipelineOwner {
return true;
}());
FlutterTimeline.startSync(
'PAINT',
'PAINT$_debugRootSuffixForTimelineEventNames',
arguments: debugTimelineArguments,
);
}
@ -1247,7 +1247,7 @@ class PipelineOwner {
return;
}
if (!kReleaseMode) {
FlutterTimeline.startSync('SEMANTICS');
FlutterTimeline.startSync('SEMANTICS$_debugRootSuffixForTimelineEventNames');
}
assert(_semanticsOwner != null);
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
final Set<PipelineOwner> _children = <PipelineOwner>{};
@ -1290,6 +1304,8 @@ class PipelineOwner {
return true;
}
String get _debugRootSuffixForTimelineEventNames => _debugParent == null ? ' (root)' : '';
/// Mark this [PipelineOwner] as attached to the given [PipelineManifold].
///
/// Typically, this is only called directly on the root [PipelineOwner].
@ -1315,7 +1331,9 @@ class PipelineOwner {
assert(_manifold != null);
_manifold!.removeListener(_updateSemanticsOwner);
_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) {
child.detach();
@ -1351,7 +1369,9 @@ class PipelineOwner {
assert(!_children.contains(child));
assert(_debugAllowChildListModifications, 'Cannot modify child list after layout.');
_children.add(child);
assert(_debugSetParent(child, this));
if (!kReleaseMode) {
_debugSetParent(child, this);
}
if (_manifold != null) {
child.attach(_manifold!);
}
@ -1369,7 +1389,9 @@ class PipelineOwner {
assert(_children.contains(child));
assert(_debugAllowChildListModifications, 'Cannot modify child list after layout.');
_children.remove(child);
assert(_debugSetParent(child, null));
if (!kReleaseMode) {
_debugSetParent(child, null);
}
if (_manifold != null) {
child.detach();
}
@ -1384,6 +1406,26 @@ class PipelineOwner {
void visitChildren(PipelineOwnerVisitor 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].
@ -3919,7 +3961,6 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
/// This mixin is typically used to implement render objects created
/// in a [SingleChildRenderObjectWidget].
mixin RenderObjectWithChildMixin<ChildType extends RenderObject> on RenderObject {
/// Checks whether the given render object has the correct [runtimeType] to be
/// 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]).
///
/// 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({
RenderBox? child,
required ViewConfiguration configuration,
ViewConfiguration? configuration,
required ui.FlutterView view,
}) : _configuration = configuration,
_view = view {
@ -82,26 +86,39 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
Size _size = Size.zero;
/// 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) {
if (configuration == value) {
if (_configuration == value) {
return;
}
final ViewConfiguration oldConfiguration = _configuration;
final ViewConfiguration? oldConfiguration = _configuration;
_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());
}
assert(_rootTransform != null);
markNeedsLayout();
}
/// Whether a [configuration] has been set.
bool get hasConfiguration => _configuration != null;
/// The [FlutterView] into which this [RenderView] will render.
ui.FlutterView get 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 [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 {
@override
void initInstances() {
@ -975,6 +1017,8 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
Widget wrapWithDefaultView(Widget rootWidget) {
return View(
view: platformDispatcher.implicitView!,
deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: pipelineOwner,
deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: renderView,
child: rootWidget,
);
}
@ -1000,13 +1044,25 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
/// * [RenderObjectToWidgetAdapter.attachToRenderTree], which inflates a
/// widget and attaches it to the render tree.
void attachRootWidget(Widget rootWidget) {
final bool isBootstrapFrame = rootElement == null;
_readyToProduceFrames = true;
_rootElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
attachToBuildOwner(RootWidget(
debugShortDescription: '[root]',
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) {
SchedulerBinding.instance.ensureVisualUpdate();
}
@ -1121,52 +1177,40 @@ void debugDumpApp() {
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
/// 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.
/// Exposes an [attach] method to attach the widget tree to a [BuildOwner]. That
/// method also bootstraps the element tree.
///
/// Used by [runApp] to bootstrap applications.
class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWidget {
/// Creates a bridge from a [RenderObject] to an [Element] tree.
///
/// Used by [WidgetsBinding] to attach the root widget to the [RenderView].
RenderObjectToWidgetAdapter({
/// Used by [WidgetsBinding.attachRootWidget] (which is indirectly called by
/// [runApp]) to bootstrap applications.
class RootWidget extends Widget {
/// Creates a [RootWidget].
const RootWidget({
super.key,
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);
RootElement createElement() => RootElement(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].
/// Inflate this widget and attaches it to the provided [BuildOwner].
///
/// 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.
///
/// Used by [runApp] to bootstrap applications.
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T>? element ]) {
/// Used by [WidgetsBinding.attachToBuildOwner] (which is indirectly called by
/// [runApp]) to bootstrap applications.
RootElement attach(BuildOwner owner, [ RootElement? element ]) {
if (element == null) {
owner.lockState(() {
element = createElement();
@ -1174,7 +1218,7 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi
element!.assignOwner(owner);
});
owner.buildScope(element!, () {
element!.mount(null, null);
element!.mount(/* parent */ null, /* slot */ null);
});
} else {
element._newWidget = this;
@ -1187,28 +1231,22 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi
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]
/// 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).
/// This element class is the instantiation of a [RootWidget]. It can be used
/// only as the root of an [Element] tree (it cannot be mounted into another
/// [Element]; its parent must be null).
///
/// In typical usage, it will be instantiated for a [RenderObjectToWidgetAdapter]
/// whose container is the [RenderView] that connects to the Flutter engine. In
/// this usage, it is normally instantiated by the bootstrapping logic in the
/// [WidgetsFlutterBinding] singleton created by [runApp].
class RenderObjectToWidgetElement<T extends RenderObject> extends RenderObjectElement 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);
/// In typical usage, it will be instantiated for a [RootWidget] by calling
/// [RootWidget.attach]. In this usage, it is normally instantiated by the
/// bootstrapping logic in the [WidgetsFlutterBinding] singleton created by
/// [runApp].
class RootElement extends Element with RootElementMixin {
/// Creates a [RootElement] for the provided [RootWidget].
RootElement(RootWidget super.widget);
Element? _child;
static const Object _rootChildSlot = Object();
@override
void visitChildren(ElementVisitor visitor) {
if (_child != null) {
@ -1225,14 +1263,15 @@ class RenderObjectToWidgetElement<T extends RenderObject> extends RenderObjectEl
@override
void mount(Element? parent, Object? newSlot) {
assert(parent == null);
assert(parent == null); // We are the root!
super.mount(parent, newSlot);
_rebuild();
assert(_child != null);
super.performRebuild(); // clears the "dirty" flag
}
@override
void update(RenderObjectToWidgetAdapter<T> newWidget) {
void update(RootWidget newWidget) {
super.update(newWidget);
assert(widget == newWidget);
_rebuild();
@ -1240,25 +1279,24 @@ class RenderObjectToWidgetElement<T extends RenderObject> extends RenderObjectEl
// When we are assigned a new widget, we store it here
// until we are ready to update to it.
Widget? _newWidget;
RootWidget? _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!;
final RootWidget newWidget = _newWidget!;
_newWidget = null;
update(newWidget as RenderObjectToWidgetAdapter<T>);
update(newWidget);
}
super.performRebuild();
assert(_newWidget == null);
}
@pragma('vm:notify-debugger-on-exception')
void _rebuild() {
try {
_child = updateChild(_child, (widget as RenderObjectToWidgetAdapter<T>).child, _rootChildSlot);
_child = updateChild(_child, (widget as RootWidget).child, /* slot */ null);
} catch (exception, stack) {
final FlutterErrorDetails details = FlutterErrorDetails(
exception: exception,
@ -1267,31 +1305,18 @@ class RenderObjectToWidgetElement<T extends RenderObject> extends RenderObjectEl
context: ErrorDescription('attaching to the render tree'),
);
FlutterError.reportError(details);
final Widget error = ErrorWidget.builder(details);
_child = updateChild(null, error, _rootChildSlot);
// No error widget possible here since it wouldn't have a view to render into.
_child = null;
}
}
@override
RenderObjectWithChildMixin<T> get renderObject => super.renderObject as RenderObjectWithChildMixin<T>;
bool get debugDoingBuild => false; // This element doesn't have a build phase.
@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;
}
// There is no ancestor RenderObjectElement that the render object could be attached to.
bool debugExpectsRenderObjectForSlot(Object? slot) => false;
}
/// 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
/// this location in the tree. Otherwise, this getter will walk down the tree
/// 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 {
Element? current = this;
while (current != null) {
@ -3460,17 +3465,33 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
} else if (current is RenderObjectElement) {
return current.renderObject;
} else {
Element? next;
current.visitChildren((Element child) {
assert(next == null); // This verifies that there's only one child.
next = child;
});
current = next;
current = current.renderObjectAttachingChild;
}
}
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
List<DiagnosticsNode> describeMissingAncestor({ required Type expectedAncestorType }) {
final List<DiagnosticsNode> information = <DiagnosticsNode>[];
@ -4021,15 +4042,20 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
assert(_lifecycleState == _ElementLifecycle.active);
assert(child._parent == this);
void visit(Element element) {
element._updateSlot(newSlot);
if (element is! RenderObjectElement) {
element.visitChildren(visit);
element.updateSlot(newSlot);
final Element? descendant = element.renderObjectAttachingChild;
if (descendant != null) {
visit(descendant);
}
}
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(_parent != null);
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].
void attachRenderObject(Object? newSlot) {
assert(_slot == null);
assert(slot == null);
visitChildren((Element child) {
child.attachRenderObject(newSlot);
});
@ -4143,7 +4169,6 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
@protected
@pragma('vm:prefer-inline')
Element inflateWidget(Widget newWidget, Object? newSlot) {
final bool isTimelineTracked = !kReleaseMode && _isProfileBuildsEnabledFor(newWidget);
if (isTimelineTracked) {
Map<String, String>? debugTimelineArguments;
@ -4169,7 +4194,17 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
_debugCheckForCycles(newChild);
return true;
}());
try {
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);
assert(newChild == updatedChild);
return updatedChild!;
@ -4404,6 +4439,33 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
_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
RenderObject? findRenderObject() {
assert(() {
@ -5266,6 +5328,9 @@ abstract class ComponentElement extends Element {
@override
bool get debugDoingBuild => _debugDoingBuild;
@override
Element? get renderObjectAttachingChild => _child;
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
@ -6073,6 +6138,9 @@ abstract class RenderObjectElement extends Element {
}
RenderObject? _renderObject;
@override
Element? get renderObjectAttachingChild => null;
bool _debugDoingBuild = false;
@override
bool get debugDoingBuild => _debugDoingBuild;
@ -6082,8 +6150,25 @@ abstract class RenderObjectElement extends Element {
RenderObjectElement? _findAncestorRenderObjectElement() {
Element? ancestor = _parent;
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?;
}
@ -6151,7 +6236,7 @@ abstract class RenderObjectElement extends Element {
_debugUpdateRenderObjectOwner();
return true;
}());
assert(_slot == newSlot);
assert(slot == newSlot);
attachRenderObject(newSlot);
super.performRebuild(); // clears the "dirty" flag
}
@ -6252,12 +6337,13 @@ abstract class RenderObjectElement extends Element {
}
@override
void _updateSlot(Object? newSlot) {
void updateSlot(Object? newSlot) {
final Object? oldSlot = slot;
assert(oldSlot != newSlot);
super._updateSlot(newSlot);
super.updateSlot(newSlot);
assert(slot == newSlot);
_ancestorRenderObjectElement!.moveRenderObjectChild(renderObject, oldSlot, slot);
assert(_ancestorRenderObjectElement == _findAncestorRenderObjectElement());
_ancestorRenderObjectElement?.moveRenderObjectChild(renderObject, oldSlot, slot);
}
@override
@ -6265,6 +6351,25 @@ abstract class RenderObjectElement extends Element {
assert(_ancestorRenderObjectElement == null);
_slot = newSlot;
_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);
final ParentDataElement<ParentData>? parentDataElement = _findAncestorParentDataElement();
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].
///
/// 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 {
late _SemanticsClient _client;
_SemanticsClient? _client;
PipelineOwner? _pipelineOwner;
@override
void 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);
}
@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
void dispose() {
_client
..removeListener(_update)
..dispose();
_client?.dispose();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@ -145,19 +151,15 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> with WidgetsBindi
}
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
Widget build(BuildContext context) {
return CustomPaint(
foregroundPainter: _SemanticsDebuggerPainter(
_pipelineOwner,
_client.generation,
_pipelineOwner!,
_client!.generation,
_lastPointerDownLocation, // in physical pixels
View.of(context).devicePixelRatio,
widget.labelStyle,

View File

@ -2,48 +2,89 @@
// Use of this source code is governed by a BSD-style license that can be
// 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 'lookup_boundary.dart';
import 'media_query.dart';
/// Injects a [FlutterView] into the tree and makes it available to descendants
/// within the same [LookupBoundary] via [View.of] and [View.maybeOf].
/// Bootstraps a render tree that is rendered into the provided [FlutterView].
///
/// 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
/// [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
/// widget tree. Two or more [View] widgets configured with the same
/// [FlutterView] must never exist within the same widget tree at the same time.
/// Internally, this limitation is enforced by a [GlobalObjectKey] that derives
/// its identity from the [view] provided to this widget.
/// This limitation is enforced by a [GlobalObjectKey] that derives its identity
/// 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 {
/// Injects the provided [view] into the widget tree.
View({required this.view, required this.child}) : super(key: GlobalObjectKey(view));
/// Create a [View] widget to bootstrap a render tree that is rendered into
/// 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;
/// The widget below this widget in the tree, which will be drawn into the
/// [view].
///
/// {@macro flutter.widgets.ProxyWidget.child}
final Widget child;
@override
Widget build(BuildContext context) {
return _ViewScope(
view: view,
child: MediaQuery.fromView(
view: view,
child: child,
),
);
}
final PipelineOwner? _deprecatedPipelineOwner;
final RenderView? _deprecatedRenderView;
/// Returns the [FlutterView] that the provided `context` will render into.
///
@ -106,13 +147,588 @@ class View extends StatelessWidget {
}());
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 {
const _ViewScope({required this.view, required super.child});
final FlutterView view;
final FlutterView? view;
@override
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.visitChildren(markTreeNeedsPaint);
}
final RenderObject root = RendererBinding.instance.renderView;
markTreeNeedsPaint(root);
RendererBinding.instance.renderViews.forEach(markTreeNeedsPaint);
} else {
debugOnProfilePaint = null;
}

View File

@ -18,6 +18,7 @@ export 'package:vector_math/vector_math_64.dart' show Matrix4;
export 'foundation.dart' show UniqueKey;
export 'rendering.dart' show TextSelectionHandleType;
export 'src/widgets/actions.dart';
export 'src/widgets/adapter.dart';
export 'src/widgets/animated_cross_fade.dart';
export 'src/widgets/animated_scroll_view.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() {
final List<String?> console = <String?>[];
late PipelineOwner owner;
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);
// We need to test this service extension here because the result is true
@ -176,6 +184,10 @@ void main() {
expect(console, isEmpty);
debugPrint = debugPrintThrottled;
binding.rootPipelineOwner.dropChild(owner);
owner
..rootNode = null
..dispose();
});
// The following list is alphabetical, one test per extension.
@ -268,11 +280,13 @@ void main() {
await binding.doFrame();
final Map<String, dynamic> result = await binding.testExtension(RenderingServiceExtensions.debugDumpSemanticsTreeInTraversalOrder.name, <String, String>{});
expect(result, <String, String>{
'data': '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.'
expect(result, <String, Object>{
'data': matches(
r'Semantics not generated for RenderView#[0-9a-f]{5}\.\n'
r'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n'
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();
final Map<String, dynamic> result = await binding.testExtension(RenderingServiceExtensions.debugDumpSemanticsTreeInInverseHitTestOrder.name, <String, String>{});
expect(result, <String, String>{
'data': '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.'
expect(result, <String, Object>{
'data': matches(
r'Semantics not generated for RenderView#[0-9a-f]{5}\.\n'
r'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n'
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(() {
final List<PipelineOwner> children = <PipelineOwner>[];
RendererBinding.instance.pipelineOwner.visitChildren((PipelineOwner child) {
RendererBinding.instance.rootPipelineOwner.visitChildren((PipelineOwner 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", () {
final PipelineOwner child = PipelineOwner();
RendererBinding.instance.pipelineOwner.adoptChild(child);
RendererBinding.instance.rootPipelineOwner.adoptChild(child);
final RenderObject renderObject = TestRenderObject();
child.rootNode = renderObject;
renderObject.scheduleInitialLayout();
RendererBinding.instance.pipelineOwner.flushLayout();
RendererBinding.instance.rootPipelineOwner.flushLayout();
MyTestRenderingFlutterBinding.instance.ensureVisualUpdateCount = 0;
renderObject.markNeedsLayout();
@ -37,20 +37,20 @@ void main() {
final PipelineOwner child = PipelineOwner(
onSemanticsUpdate: (_) { },
);
RendererBinding.instance.pipelineOwner.adoptChild(child);
RendererBinding.instance.rootPipelineOwner.adoptChild(child);
expect(child.semanticsOwner, isNull);
expect(RendererBinding.instance.pipelineOwner.semanticsOwner, isNull);
expect(RendererBinding.instance.rootPipelineOwner.semanticsOwner, isNull);
final SemanticsHandle handle = SemanticsBinding.instance.ensureSemantics();
expect(child.semanticsOwner, isNotNull);
expect(RendererBinding.instance.pipelineOwner.semanticsOwner, isNotNull);
expect(RendererBinding.instance.rootPipelineOwner.semanticsOwner, isNotNull);
handle.dispose();
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() {
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);
RendererBinding.instance.handleMetricsChanged();
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.handleMetricsChanged();
expect(SchedulerBinding.instance.hasScheduledFrame, true);
RendererBinding.instance.removeRenderView(RendererBinding.instance.renderView);
});
test('debugDumpSemantics prints explanation when semantics are unavailable', () {
RendererBinding.instance.addRenderView(RendererBinding.instance.renderView);
final List<String?> log = <String?>[];
debugPrint = (String? message, {int? wrapWidth}) {
log.add(message);
};
debugDumpSemanticsTree();
expect(log, hasLength(1));
expect(
log.single,
'Semantics not generated.\n'
expect(log.single, startsWith('Semantics not generated'));
expect(log.single, endsWith(
'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.'
));
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)>[];
}
late final RenderView _renderView = RenderView(
view: platformDispatcher.implicitView!,
);
late final PipelineOwner _pipelineOwner = PipelineOwner(
onSemanticsUpdate: (ui.SemanticsUpdate _) { assert(false); },
);
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;

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(child.semanticsOwner, isNotNull);
expect(childOfChild.semanticsOwner, isNull);
expect(childOfChild.semanticsOwner, isNotNull); // Retained in case we get re-attached.
final SemanticsHandle childSemantics = child.ensureSemantics();
root.dropChild(child);
expect(root.semanticsOwner, isNotNull);
expect(child.semanticsOwner, isNotNull);
expect(childOfChild.semanticsOwner, isNull);
expect(childOfChild.semanticsOwner, isNotNull); // Retained in case we get re-attached.
childSemantics.dispose();
expect(root.semanticsOwner, isNotNull);
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', () {
@ -789,6 +812,38 @@ void main() {
});
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 {
@ -860,3 +915,5 @@ List<PipelineOwner> _treeWalk(PipelineOwner root) {
root.visitChildren(visitor);
return results;
}
class FakeRenderView extends RenderBox { }

View File

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

View File

@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:ui' show SemanticsUpdate;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
@ -43,6 +44,44 @@ class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, Ser
void initInstances() {
super.initInstances();
_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
@ -139,23 +178,25 @@ class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, Ser
final FlutterExceptionHandler? oldErrorHandler = FlutterError.onError;
FlutterError.onError = _errors.add;
try {
pipelineOwner.flushLayout();
rootPipelineOwner.flushLayout();
if (phase == EnginePhase.layout) {
return;
}
pipelineOwner.flushCompositingBits();
rootPipelineOwner.flushCompositingBits();
if (phase == EnginePhase.compositingBits) {
return;
}
pipelineOwner.flushPaint();
rootPipelineOwner.flushPaint();
if (phase == EnginePhase.paint) {
return;
}
for (final RenderView renderView in renderViews) {
renderView.compositeFrame();
}
if (phase == EnginePhase.composite) {
return;
}
pipelineOwner.flushSemantics();
rootPipelineOwner.flushSemantics();
if (phase == EnginePhase.flushSemantics) {
return;
}

View File

@ -122,6 +122,16 @@ void main() {
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);

View File

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

View File

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

View File

@ -30,7 +30,7 @@ class TestWidgetState extends State<TestWidget> {
void main() {
testWidgets('initState() is called when we are in the tree', (WidgetTester tester) async {
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 "<----"
expect(tester.binding.renderView.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes(
'RenderView#00000\n'
'_ReusableRenderView#00000\n'
' │ debug mode enabled - ${Platform.operatingSystem}\n'
' │ view size: Size(2400.0, 1800.0) (in physical pixels)\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.pump();
expect(tester.binding.renderView.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes(
'RenderView#00000\n'
'_ReusableRenderView#00000\n'
' │ debug mode enabled - ${Platform.operatingSystem}\n'
' │ view size: Size(2400.0, 1800.0) (in physical pixels)\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() {
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,
// which introduces a MediaQuery ancestor.
await pumpWidgetWithoutViewWrapper(
tester: tester,
widget: Builder(
builder: (BuildContext context) {
tested = true;
MediaQuery.of(context); // should throw
return Container();
try {
MediaQuery.of(context);
} 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(
error.toStringDeep(),
startsWith(
@ -119,7 +118,10 @@ void main() {
final MediaQueryData? data = MediaQuery.maybeOf(context);
expect(data, isNull);
tested = true;
return Container();
return View(
view: tester.view,
child: const SizedBox(),
);
},
),
);
@ -295,7 +297,10 @@ void main() {
child: Builder(
builder: (BuildContext 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) {
rebuildCount++;
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(
'_RenderDiagonal#00000 relayoutBoundary=up1\n'
' │ creator: _Diagonal ← Align ← Directionality ← MediaQuery ←\n'
' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n'
' │ TestFlutterView#00000] ← [root]\n'
' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n'
' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
' │ [root]\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'
' │ size: Size(190.0, 220.0)\n'
'\n'
' ├─topLeft: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
' │ creator: SizedBox ← _Diagonal ← Align ← Directionality ←\n'
' │ MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n'
' │ View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n'
' │ MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope ←\n'
' │ _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
' │ TestFlutterView#00000] ← View ← [root]\n'
' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
' │ constraints: BoxConstraints(unconstrained)\n'
' │ size: Size(80.0, 100.0)\n'
@ -239,8 +241,9 @@ void main() {
'\n'
' └─bottomRight: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
' creator: SizedBox ← _Diagonal ← Align ← Directionality ←\n'
' MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n'
' View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n'
' MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope ←\n'
' _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
' TestFlutterView#00000] ← View ← [root]\n'
' parentData: offset=Offset(80.0, 100.0) (can use size)\n'
' constraints: BoxConstraints(unconstrained)\n'
' size: Size(110.0, 120.0)\n'

View File

@ -10,10 +10,9 @@ import 'test_widgets.dart';
void main() {
testWidgets('Stateful widget smoke test', (WidgetTester tester) async {
void checkTree(BoxDecoration expectedDecoration) {
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.renderObject, isA<RenderDecoratedBox>());

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -187,11 +187,20 @@ mixin CommandHandlerFactory {
Future<Health> _getHealth(Command command) async => const Health(HealthStatus.ok);
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 {
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 {

View File

@ -58,8 +58,8 @@ class MatchesGoldenFile extends AsyncMatcher {
final RenderObject renderObject = _findRepaintBoundary(element);
final Size size = renderObject.paintBounds.size;
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance;
final Element e = binding.rootElement!;
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
// to an image directly. Instead, we will use `window.render()` to render
@ -78,7 +78,7 @@ class MatchesGoldenFile extends AsyncMatcher {
return ex.message;
}
});
_renderElement(view, _findRepaintBoundary(e));
_renderElement(view, renderView);
return result;
}

View File

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

View File

@ -495,8 +495,8 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
Size? _surfaceSize;
/// Artificially changes the surface size to `size` on the Widget binding,
/// then flushes microtasks.
/// Artificially changes the logical size of [WidgetTester.view] to the
/// specified size, then flushes microtasks.
///
/// Set to null to use the default surface size.
///
@ -508,7 +508,10 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
/// 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
Future<void> setSurfaceSize(Size? size) {
return TestAsyncUtils.guard<void>(() async {
@ -522,15 +525,38 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
}
@override
ViewConfiguration createViewConfiguration() {
final FlutterView view = platformDispatcher.implicitView!;
final double devicePixelRatio = view.devicePixelRatio;
final Size size = _surfaceSize ?? view.physicalSize / devicePixelRatio;
void addRenderView(RenderView view) {
_insideAddRenderView = true;
try {
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(
size: size,
devicePixelRatio: devicePixelRatio,
size: _surfaceSize!,
devicePixelRatio: view.devicePixelRatio,
);
}
return super.createViewConfigurationFor(renderView);
}
/// Acts as if the application went idle.
///
@ -1377,16 +1403,18 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
debugBuildingDirtyElements = true;
buildOwner!.buildScope(rootElement!);
if (_phase != EnginePhase.build) {
pipelineOwner.flushLayout();
rootPipelineOwner.flushLayout();
if (_phase != EnginePhase.layout) {
pipelineOwner.flushCompositingBits();
rootPipelineOwner.flushCompositingBits();
if (_phase != EnginePhase.compositingBits) {
pipelineOwner.flushPaint();
rootPipelineOwner.flushPaint();
if (_phase != EnginePhase.paint && sendFramesToEngine) {
_firstFrameSent = true;
for (final RenderView renderView in renderViews) {
renderView.compositeFrame(); // this sends the bits to the GPU
}
if (_phase != EnginePhase.composite) {
pipelineOwner.flushSemantics();
rootPipelineOwner.flushSemantics(); // this sends the semantics to the OS.
assert(_phase == EnginePhase.flushSemantics ||
_phase == EnginePhase.sendSemanticsUpdate);
}
@ -1759,10 +1787,15 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
}
}
void _markViewNeedsPaint() {
void _markViewsNeedPaint([int? viewId]) {
_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();
}
}
TextPainter? _label;
static const TextStyle _labelStyle = TextStyle(
@ -1779,15 +1812,16 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
_label ??= TextPainter(textAlign: TextAlign.left, textDirection: TextDirection.ltr);
_label!.text = TextSpan(text: value, style: _labelStyle);
_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) {
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 Path path = Path()
..addOval(Rect.fromCircle(center: Offset.zero, radius: radius))
@ -1800,7 +1834,7 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
..strokeWidth = radius / 10.0
..style = PaintingStyle.stroke;
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);
canvas.drawPath(path.shift(record.position), paint);
if (record.decay < 0) {
@ -1808,14 +1842,14 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
}
record.decay += 1;
}
_pointerIdToPointerRecord
pointerIdToRecord
.keys
.where((int pointer) => _pointerIdToPointerRecord[pointer]!.decay == 0)
.where((int pointer) => pointerIdToRecord[pointer]!.decay == 0)
.toList()
.forEach(_pointerIdToPointerRecord.remove);
.forEach(pointerIdToRecord.remove);
if (dirty) {
scheduleMicrotask(() {
_markViewNeedsPaint();
_markViewsNeedPaint(renderView.flutterView.viewId);
});
}
}
@ -1846,19 +1880,29 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
void handlePointerEvent(PointerEvent event) {
switch (pointerEventSource) {
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) {
record.position = event.position;
if (!event.down) {
record.decay = _kPointerDecay;
}
_markViewNeedsPaint();
_markViewsNeedPaint(event.viewId);
} else if (event.down) {
_pointerIdToPointerRecord[event.pointer] = _LiveTestPointerRecord(
_renderViewToPointerIdToPointerRecord[target] ??= <int, _LiveTestPointerRecord>{};
_renderViewToPointerIdToPointerRecord[target]![event.pointer] = _LiveTestPointerRecord(
event.pointer,
event.position,
);
_markViewNeedsPaint();
_markViewsNeedPaint(event.viewId);
}
}
super.handlePointerEvent(event);
case TestBindingEventSource.device:
@ -1870,6 +1914,7 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
// The pointer events received with this source has a global position
// (see [handlePointerEventForSource]). Transform it to the local
// 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));
withPointerEventSource(TestBindingEventSource.device,
() => super.handlePointerEvent(localEvent)
@ -1987,10 +2032,18 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
}
@override
ViewConfiguration createViewConfiguration() {
ViewConfiguration createViewConfigurationFor(RenderView renderView) {
final FlutterView view = renderView.flutterView;
if (view == platformDispatcher.implicitView) {
return TestViewConfiguration.fromView(
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
/// automatically created as part of instantiating a [WidgetController], but
/// a custom implementation can be passed via the [WidgetController] constructor.
SemanticsController._(WidgetsBinding binding) : _binding = binding;
SemanticsController._(this._controller);
static final int _scrollingActions =
SemanticsAction.scrollUp.index |
@ -55,7 +55,7 @@ class SemanticsController {
SemanticsFlag.isSlider.index |
SemanticsFlag.isInMutuallyExclusiveGroup.index;
final WidgetsBinding _binding;
final WidgetController _controller;
/// 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.
SemanticsNode find(Finder finder) {
TestAsyncUtils.guardSync();
if (!_binding.semanticsEnabled) {
if (!_controller.binding.semanticsEnabled) {
throw StateError('Semantics are not enabled.');
}
final Iterable<Element> candidates = finder.evaluate();
@ -109,6 +109,13 @@ class SemanticsController {
/// tree. If `end` finds zero elements or more than one element, a
/// [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
/// (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
@ -139,10 +146,47 @@ class SemanticsController {
/// parts of the traversal.
/// * [orderedEquals], which can be given an [Iterable<Matcher>] to exactly
/// match the order of the traversal.
Iterable<SemanticsNode> simulatedAccessibilityTraversal({Finder? start, Finder? end}) {
Iterable<SemanticsNode> simulatedAccessibilityTraversal({Finder? start, Finder? end, FlutterView? view}) {
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>[];
_traverse(_binding.pipelineOwner.semanticsOwner!.rootSemanticsNode!, traversal);
_traverse(renderView.owner!.semanticsOwner!.rootSemanticsNode!, traversal);
int startIndex = 0;
int endIndex = traversal.length - 1;
@ -229,8 +273,7 @@ class SemanticsController {
/// Concrete subclasses must implement the [pump] method.
abstract class WidgetController {
/// Creates a widget controller that uses the given binding.
WidgetController(this.binding)
: _semantics = SemanticsController._(binding);
WidgetController(this.binding);
/// A reference to the current instance of the binding.
final WidgetsBinding binding;
@ -280,7 +323,7 @@ abstract class WidgetController {
return _semantics;
}
final SemanticsController _semantics;
late final SemanticsController _semantics = SemanticsController._(this);
// FINDER API
@ -297,14 +340,16 @@ abstract class WidgetController {
/// * [view] which returns the [TestFlutterView] used when only a single
/// view is being used.
TestFlutterView viewOf(Finder finder) {
final View view = firstWidget<View>(
return _viewOf(finder) as TestFlutterView;
}
FlutterView _viewOf(Finder finder) {
return firstWidget<View>(
find.ancestor(
of: finder,
matching: find.byType(View),
)
);
return view.view as TestFlutterView;
),
).view;
}
/// 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.
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* {
TestAsyncUtils.guardSync();
yield layer;
@ -1190,10 +1240,10 @@ abstract class WidgetController {
}
/// 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();
// TODO(goderbauer): Support multiple views in flutter_test pointer event handling, https://github.com/flutter/flutter/issues/128281
binding.hitTest(result, location); // ignore: deprecated_member_use
binding.hitTestInView(result, location, viewId);
return result;
}
@ -1313,9 +1363,9 @@ abstract class WidgetController {
final RenderBox box = element.renderObject! as RenderBox;
final Offset location = box.localToGlobal(sizeToPoint(box.size));
if (warnIfMissed) {
final FlutterView view = _viewOf(finder);
final HitTestResult result = HitTestResult();
// TODO(goderbauer): Support multiple views in flutter_test pointer event handling, https://github.com/flutter/flutter/issues/128281
binding.hitTest(result, location); // ignore: deprecated_member_use
binding.hitTestInView(result, location, view.viewId);
bool found = false;
for (final HitTestEntry entry in result.path) {
if (entry.target == box) {
@ -1324,15 +1374,16 @@ abstract class WidgetController {
}
}
if (!found) {
final RenderView renderView = binding.renderViews.firstWhere((RenderView r) => r.flutterView == view);
bool outOfBounds = false;
outOfBounds = !(Offset.zero & binding.renderView.size).contains(location);
outOfBounds = !(Offset.zero & renderView.size).contains(location);
if (hitTestWarningShouldBeFatal) {
throw FlutterError.fromParts(<DiagnosticsNode>[
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.'),
ErrorHint('Maybe the widget is actually off-screen, or another widget is obscuring it, or the widget cannot receive pointer events.'),
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),
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()".'),
@ -1343,7 +1394,7 @@ abstract class WidgetController {
'\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'
'${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 hit test result at that offset is: $result\n'
'${StackTrace.current}'

View File

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

View File

@ -569,22 +569,10 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
EnginePhase phase = EnginePhase.sendSemanticsUpdate,
]) {
return TestAsyncUtils.guard<void>(() {
return _pumpWidget(
binding.wrapWithDefaultView(widget),
duration,
phase,
);
});
}
Future<void> _pumpWidget(
Widget widget, [
Duration? duration,
EnginePhase phase = EnginePhase.sendSemanticsUpdate,
]) {
binding.attachRootWidget(widget);
binding.attachRootWidget(binding.wrapWithDefaultView(widget));
binding.scheduleFrame();
return binding.pump(duration, phase);
});
}
@override
@ -745,12 +733,14 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
'your widget tree in a RootRestorationScope?',
);
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;
runApp(Container(key: UniqueKey()));
await pump();
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;
@override
HitTestResult hitTestOnBinding(Offset location) {
location = binding.localToGlobal(location, binding.renderView);
return super.hitTestOnBinding(location);
HitTestResult hitTestOnBinding(Offset location, {int? viewId}) {
viewId ??= view.viewId;
final RenderView renderView = binding.renderViews.firstWhere((RenderView r) => r.flutterView.viewId == viewId);
location = binding.localToGlobal(location, renderView);
return super.hitTestOnBinding(location, viewId: viewId);
}
@override
@ -861,7 +853,9 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
.map((HitTestEntry candidate) => candidate.target)
.whereType<RenderObject>()
.first;
final Element? innerTargetElement = _lastWhereOrNull(
final Element? innerTargetElement = binding.renderViews.contains(innerTarget)
? null
: _lastWhereOrNull(
collectAllElementsFrom(binding.rootElement!, skipOffstage: true),
(Element element) => element.renderObject == innerTarget,
);
@ -1060,6 +1054,8 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
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;
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() {
testWidgets('open SemanticsHandle from PipelineOwner fails test', (WidgetTester tester) async {
final int outstandingHandles = tester.binding.pipelineOwner.debugOutstandingSemanticsHandles;
tester.binding.pipelineOwner.ensureSemantics();
expect(tester.binding.pipelineOwner.debugOutstandingSemanticsHandles, outstandingHandles + 1);
final int outstandingHandles = tester.binding.debugOutstandingSemanticsHandles;
tester.binding.ensureSemantics();
expect(tester.binding.debugOutstandingSemanticsHandles, outstandingHandles + 1);
// SemanticsHandle is not disposed on purpose to verify in tearDown that
// the test failed due to an active SemanticsHandle.
});

View File

@ -4,6 +4,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
// 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));
try {
await tester.pump();
@ -126,6 +127,7 @@ class _MockLiveTestWidgetsFlutterBinding extends LiveTestWidgetsFlutterBinding {
// real devices touches sends event in the global coordinate system.
// See the documentation of [handlePointerEventForSource] for details.
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));
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.
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
Future<void> setSurfaceSize(Size? size) {
return TestAsyncUtils.guard<void>(() async {
@ -117,12 +113,11 @@ https://flutter.dev/docs/testing/integration-tests#testing-on-firebase-test-lab
}
@override
ViewConfiguration createViewConfiguration() {
final FlutterView view = platformDispatcher.implicitView!;
final double devicePixelRatio = view.devicePixelRatio;
final Size size = _surfaceSize ?? view.physicalSize / devicePixelRatio;
ViewConfiguration createViewConfigurationFor(RenderView renderView) {
final FlutterView view = renderView.flutterView;
final Size? surfaceSize = view == platformDispatcher.implicitView ? _surfaceSize : null;
return TestViewConfiguration.fromView(
size: size,
size: surfaceSize ?? view.physicalSize / view.devicePixelRatio,
view: view,
);
}
@ -442,11 +437,11 @@ https://flutter.dev/docs/testing/integration-tests#testing-on-firebase-test-lab
Timeout defaultTestTimeout = Timeout.none;
@override
void attachRootWidget(Widget rootWidget) {
Widget wrapWithDefaultView(Widget rootWidget) {
// This is a workaround where screenshots of root widgets have incorrect
// bounds.
// 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