mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
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:
parent
7a42ed7ef6
commit
6f09064e78
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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.
|
||||
|
69
examples/layers/rendering/src/binding.dart
Normal file
69
examples/layers/rendering/src/binding.dart
Normal 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();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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) {
|
||||
renderView.compositeFrame(); // this sends the bits to the GPU
|
||||
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
|
||||
for (final RenderView renderView in renderViews) {
|
||||
renderView.compositeFrame(); // this sends the bits to the GPU
|
||||
}
|
||||
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 {
|
||||
renderView.reassemble();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
///
|
||||
|
@ -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;
|
||||
|
177
packages/flutter/lib/src/widgets/adapter.dart
Normal file
177
packages/flutter/lib/src/widgets/adapter.dart
Normal 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;
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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;
|
||||
}());
|
||||
newChild._activateWithParent(this, newSlot);
|
||||
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
|
||||
|
@ -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,
|
||||
|
@ -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)}]';
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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.'
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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, 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(
|
||||
log.single,
|
||||
'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.'
|
||||
() => RendererBinding.instance.rootPipelineOwner.rootNode = rootNode,
|
||||
throwsA(isFlutterError.having(
|
||||
(FlutterError e) => e.message,
|
||||
'message',
|
||||
contains('Cannot set a rootNode on the default root pipeline owner.'),
|
||||
)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
|
208
packages/flutter/test/rendering/multi_view_binding_test.dart
Normal file
208
packages/flutter/test/rendering/multi_view_binding_test.dart
Normal 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));
|
||||
}
|
||||
}
|
@ -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 { }
|
||||
|
@ -47,10 +47,10 @@ void main() {
|
||||
child: platformViewRenderBox,
|
||||
);
|
||||
int semanticsUpdateCount = 0;
|
||||
final SemanticsHandle semanticsHandle = TestRenderingFlutterBinding.instance.pipelineOwner.ensureSemantics(
|
||||
listener: () {
|
||||
++semanticsUpdateCount;
|
||||
},
|
||||
final SemanticsHandle semanticsHandle = TestRenderingFlutterBinding.instance.rootPipelineOwner.ensureSemantics(
|
||||
listener: () {
|
||||
++semanticsUpdateCount;
|
||||
},
|
||||
);
|
||||
layout(tree, phase: EnginePhase.flushSemantics);
|
||||
// Initial semantics update
|
||||
|
@ -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;
|
||||
}
|
||||
renderView.compositeFrame();
|
||||
for (final RenderView renderView in renderViews) {
|
||||
renderView.compositeFrame();
|
||||
}
|
||||
if (phase == EnginePhase.composite) {
|
||||
return;
|
||||
}
|
||||
pipelineOwner.flushSemantics();
|
||||
rootPipelineOwner.flushSemantics();
|
||||
if (phase == EnginePhase.flushSemantics) {
|
||||
return;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
|
@ -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'
|
||||
),
|
||||
);
|
||||
});
|
||||
|
@ -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'
|
||||
|
@ -144,7 +144,10 @@ void main() {
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container();
|
||||
return View(
|
||||
view: tester.view,
|
||||
child: const SizedBox(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -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'
|
||||
|
@ -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']));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
39
packages/flutter/test/widgets/multi_view_binding_test.dart
Normal file
39
packages/flutter/test/widgets/multi_view_binding_test.dart
Normal 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));
|
||||
});
|
||||
}
|
221
packages/flutter/test/widgets/multi_view_tree_updates_test.dart
Normal file
221
packages/flutter/test/widgets/multi_view_tree_updates_test.dart
Normal 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;
|
||||
}
|
@ -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'
|
||||
|
@ -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>());
|
||||
|
1161
packages/flutter/test/widgets/tree_shape_test.dart
Normal file
1161
packages/flutter/test/widgets/tree_shape_test.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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() {
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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,14 +525,37 @@ 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;
|
||||
return ViewConfiguration(
|
||||
size: size,
|
||||
devicePixelRatio: 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: _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;
|
||||
renderView.compositeFrame(); // this sends the bits to the GPU
|
||||
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,9 +1787,14 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
|
||||
}
|
||||
}
|
||||
|
||||
void _markViewNeedsPaint() {
|
||||
void _markViewsNeedPaint([int? viewId]) {
|
||||
_viewNeedsPaint = true;
|
||||
renderView.markNeedsPaint();
|
||||
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;
|
||||
@ -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];
|
||||
if (record != null) {
|
||||
record.position = event.position;
|
||||
if (!event.down) {
|
||||
record.decay = _kPointerDecay;
|
||||
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;
|
||||
}
|
||||
_markViewsNeedPaint(event.viewId);
|
||||
} else if (event.down) {
|
||||
_renderViewToPointerIdToPointerRecord[target] ??= <int, _LiveTestPointerRecord>{};
|
||||
_renderViewToPointerIdToPointerRecord[target]![event.pointer] = _LiveTestPointerRecord(
|
||||
event.pointer,
|
||||
event.position,
|
||||
);
|
||||
_markViewsNeedPaint(event.viewId);
|
||||
}
|
||||
_markViewNeedsPaint();
|
||||
} else if (event.down) {
|
||||
_pointerIdToPointerRecord[event.pointer] = _LiveTestPointerRecord(
|
||||
event.pointer,
|
||||
event.position,
|
||||
);
|
||||
_markViewNeedsPaint();
|
||||
}
|
||||
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: view,
|
||||
);
|
||||
}
|
||||
final double devicePixelRatio = view.devicePixelRatio;
|
||||
return TestViewConfiguration.fromView(
|
||||
size: _surfaceSize ?? _kDefaultTestViewportSize,
|
||||
view: platformDispatcher.implicitView!,
|
||||
size: view.physicalSize / devicePixelRatio,
|
||||
view: view,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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}'
|
||||
|
@ -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;
|
||||
|
@ -569,24 +569,12 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
|
||||
EnginePhase phase = EnginePhase.sendSemanticsUpdate,
|
||||
]) {
|
||||
return TestAsyncUtils.guard<void>(() {
|
||||
return _pumpWidget(
|
||||
binding.wrapWithDefaultView(widget),
|
||||
duration,
|
||||
phase,
|
||||
);
|
||||
binding.attachRootWidget(binding.wrapWithDefaultView(widget));
|
||||
binding.scheduleFrame();
|
||||
return binding.pump(duration, phase);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _pumpWidget(
|
||||
Widget widget, [
|
||||
Duration? duration,
|
||||
EnginePhase phase = EnginePhase.sendSemanticsUpdate,
|
||||
]) {
|
||||
binding.attachRootWidget(widget);
|
||||
binding.scheduleFrame();
|
||||
return binding.pump(duration, phase);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Duration>> handlePointerEventRecord(Iterable<PointerEventRecord> records) {
|
||||
assert(records.isNotEmpty);
|
||||
@ -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,10 +853,12 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
|
||||
.map((HitTestEntry candidate) => candidate.target)
|
||||
.whereType<RenderObject>()
|
||||
.first;
|
||||
final Element? innerTargetElement = _lastWhereOrNull(
|
||||
collectAllElementsFrom(binding.rootElement!, skipOffstage: true),
|
||||
(Element element) => element.renderObject == innerTarget,
|
||||
);
|
||||
final Element? innerTargetElement = binding.renderViews.contains(innerTarget)
|
||||
? null
|
||||
: _lastWhereOrNull(
|
||||
collectAllElementsFrom(binding.rootElement!, skipOffstage: true),
|
||||
(Element element) => element.renderObject == innerTarget,
|
||||
);
|
||||
if (innerTargetElement == null) {
|
||||
printToConsole('No widgets found at ${event.position}.');
|
||||
return;
|
||||
@ -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() {
|
||||
|
135
packages/flutter_test/test/multi_view_accessibility_test.dart
Normal file
135
packages/flutter_test/test/multi_view_accessibility_test.dart
Normal 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;
|
||||
}
|
232
packages/flutter_test/test/multi_view_controller_test.dart
Normal file
232
packages/flutter_test/test/multi_view_controller_test.dart
Normal 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;
|
||||
}
|
@ -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.
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user