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),
|
size: const Size(355.0, 635.0),
|
||||||
view: tester.view,
|
view: tester.view,
|
||||||
);
|
);
|
||||||
final RenderView renderView = WidgetsBinding.instance.renderView;
|
final RenderView renderView = WidgetsBinding.instance.renderViews.single;
|
||||||
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.benchmark;
|
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.benchmark;
|
||||||
|
|
||||||
watch.start();
|
watch.start();
|
||||||
|
@ -1361,7 +1361,7 @@ Future<void> _runWebTreeshakeTest() async {
|
|||||||
final String javaScript = mainDartJs.readAsStringSync();
|
final String javaScript = mainDartJs.readAsStringSync();
|
||||||
|
|
||||||
// Check that we're not looking at minified JS. Otherwise this test would result in false positive.
|
// Check that we're not looking at minified JS. Otherwise this test would result in false positive.
|
||||||
expect(javaScript.contains('RenderObjectToWidgetElement'), true);
|
expect(javaScript.contains('RootElement'), true);
|
||||||
|
|
||||||
const String word = 'debugFillProperties';
|
const String word = 'debugFillProperties';
|
||||||
int count = 0;
|
int count = 0;
|
||||||
|
@ -79,8 +79,8 @@ Future<void> smokeDemo(WidgetTester tester, GalleryDemo demo) async {
|
|||||||
// Verify that the dumps are pretty.
|
// Verify that the dumps are pretty.
|
||||||
final String routeName = demo.routeName;
|
final String routeName = demo.routeName;
|
||||||
verifyToStringOutput('debugDumpApp', routeName, WidgetsBinding.instance.rootElement!.toStringDeep());
|
verifyToStringOutput('debugDumpApp', routeName, WidgetsBinding.instance.rootElement!.toStringDeep());
|
||||||
verifyToStringOutput('debugDumpRenderTree', routeName, RendererBinding.instance.renderView.toStringDeep());
|
verifyToStringOutput('debugDumpRenderTree', routeName, RendererBinding.instance.renderViews.single.toStringDeep());
|
||||||
verifyToStringOutput('debugDumpLayerTree', routeName, RendererBinding.instance.renderView.debugLayer?.toStringDeep() ?? '');
|
verifyToStringOutput('debugDumpLayerTree', routeName, RendererBinding.instance.renderViews.single.debugLayer?.toStringDeep() ?? '');
|
||||||
verifyToStringOutput('debugDumpFocusTree', routeName, WidgetsBinding.instance.focusManager.toStringDeep());
|
verifyToStringOutput('debugDumpFocusTree', routeName, WidgetsBinding.instance.focusManager.toStringDeep());
|
||||||
|
|
||||||
// Scroll the demo around a bit more.
|
// Scroll the demo around a bit more.
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
// system. Most of the guts of this examples are in src/sector_layout.dart.
|
// system. Most of the guts of this examples are in src/sector_layout.dart.
|
||||||
|
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'src/binding.dart';
|
||||||
import 'src/sector_layout.dart';
|
import 'src/sector_layout.dart';
|
||||||
|
|
||||||
RenderBox buildSectorExample() {
|
RenderBox buildSectorExample() {
|
||||||
@ -21,5 +22,5 @@ RenderBox buildSectorExample() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
RenderingFlutterBinding(root: buildSectorExample()).scheduleFrame();
|
ViewRenderingFlutterBinding(root: buildSectorExample()).scheduleFrame();
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
import 'src/binding.dart';
|
||||||
import 'src/solid_color_box.dart';
|
import 'src/solid_color_box.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@ -86,5 +87,5 @@ void main() {
|
|||||||
child: RenderPadding(child: table, padding: const EdgeInsets.symmetric(vertical: 50.0)),
|
child: RenderPadding(child: table, padding: const EdgeInsets.symmetric(vertical: 50.0)),
|
||||||
);
|
);
|
||||||
|
|
||||||
RenderingFlutterBinding(root: root).scheduleFrame();
|
ViewRenderingFlutterBinding(root: root).scheduleFrame();
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,11 @@
|
|||||||
|
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
import 'src/binding.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
// We use RenderingFlutterBinding to attach the render tree to the window.
|
// We use ViewRenderingFlutterBinding to attach the render tree to the window.
|
||||||
RenderingFlutterBinding(
|
ViewRenderingFlutterBinding(
|
||||||
// The root of our render tree is a RenderPositionedBox, which centers its
|
// The root of our render tree is a RenderPositionedBox, which centers its
|
||||||
// child both vertically and horizontally.
|
// child both vertically and horizontally.
|
||||||
root: RenderPositionedBox(
|
root: RenderPositionedBox(
|
||||||
|
@ -11,6 +11,8 @@ import 'package:flutter/animation.dart';
|
|||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
|
|
||||||
|
import 'src/binding.dart';
|
||||||
|
|
||||||
class NonStopVSync implements TickerProvider {
|
class NonStopVSync implements TickerProvider {
|
||||||
const NonStopVSync();
|
const NonStopVSync();
|
||||||
@override
|
@override
|
||||||
@ -42,7 +44,7 @@ void main() {
|
|||||||
child: spin,
|
child: spin,
|
||||||
);
|
);
|
||||||
// and attach it to the window.
|
// and attach it to the window.
|
||||||
RenderingFlutterBinding(root: root);
|
ViewRenderingFlutterBinding(root: root);
|
||||||
|
|
||||||
// To make the square spin, we use an animation that repeats every 1800
|
// To make the square spin, we use an animation that repeats every 1800
|
||||||
// milliseconds.
|
// milliseconds.
|
||||||
|
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/material.dart'; // Imported just for its color palette.
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
import 'src/binding.dart';
|
||||||
|
|
||||||
// Material design colors. :p
|
// Material design colors. :p
|
||||||
List<Color> _kColors = <Color>[
|
List<Color> _kColors = <Color>[
|
||||||
Colors.teal,
|
Colors.teal,
|
||||||
@ -133,5 +135,5 @@ void main() {
|
|||||||
..left = 20.0;
|
..left = 20.0;
|
||||||
|
|
||||||
// Finally, we attach the render tree we've built to the screen.
|
// Finally, we attach the render tree we've built to the screen.
|
||||||
RenderingFlutterBinding(root: stack).scheduleFrame();
|
ViewRenderingFlutterBinding(root: stack).scheduleFrame();
|
||||||
}
|
}
|
||||||
|
@ -52,10 +52,10 @@ void attachWidgetTreeToRenderTree(RenderProxyBox container) {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
child: Row(
|
child: const Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Image.network('https://flutter.dev/images/favicon.png'),
|
FlutterLogo(),
|
||||||
const Text('PRESS ME'),
|
Text('PRESS ME'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@ -102,6 +102,16 @@ void main() {
|
|||||||
transformBox = RenderTransform(child: flexRoot, transform: Matrix4.identity(), alignment: Alignment.center);
|
transformBox = RenderTransform(child: flexRoot, transform: Matrix4.identity(), alignment: Alignment.center);
|
||||||
final RenderPadding root = RenderPadding(padding: const EdgeInsets.all(80.0), child: transformBox);
|
final RenderPadding root = RenderPadding(padding: const EdgeInsets.all(80.0), child: transformBox);
|
||||||
|
|
||||||
binding.renderView.child = root;
|
// TODO(goderbauer): Create a window if embedder doesn't provide an implicit view to draw into.
|
||||||
|
assert(binding.platformDispatcher.implicitView != null);
|
||||||
|
final RenderView view = RenderView(
|
||||||
|
view: binding.platformDispatcher.implicitView!,
|
||||||
|
child: root,
|
||||||
|
);
|
||||||
|
final PipelineOwner pipelineOwner = PipelineOwner()..rootNode = view;
|
||||||
|
binding.rootPipelineOwner.adoptChild(pipelineOwner);
|
||||||
|
binding.addRenderView(view);
|
||||||
|
view.prepareInitialFrame();
|
||||||
|
|
||||||
binding.addPersistentFrameCallback(rotate);
|
binding.addPersistentFrameCallback(rotate);
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,6 @@ import 'package:flutter/scheduler.dart';
|
|||||||
import 'package:flutter/semantics.dart';
|
import 'package:flutter/semantics.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'box.dart';
|
|
||||||
import 'debug.dart';
|
import 'debug.dart';
|
||||||
import 'mouse_tracker.dart';
|
import 'mouse_tracker.dart';
|
||||||
import 'object.dart';
|
import 'object.dart';
|
||||||
@ -22,28 +21,34 @@ export 'package:flutter/gestures.dart' show HitTestResult;
|
|||||||
// Examples can assume:
|
// Examples can assume:
|
||||||
// late BuildContext context;
|
// late BuildContext context;
|
||||||
|
|
||||||
/// The glue between the render tree and the Flutter engine.
|
/// The glue between the render trees and the Flutter engine.
|
||||||
|
///
|
||||||
|
/// The [RendererBinding] manages multiple independent render trees. Each render
|
||||||
|
/// tree is rooted in a [RenderView] that must be added to the binding via
|
||||||
|
/// [addRenderView] to be considered during frame production, hit testing, etc.
|
||||||
|
/// Furthermore, the render tree must be managed by a [PipelineOwner] that is
|
||||||
|
/// part of the pipeline owner tree rooted at [rootPipelineOwner].
|
||||||
|
///
|
||||||
|
/// Adding [PipelineOwner]s and [RenderView]s to this binding in the way
|
||||||
|
/// described above is left as a responsibility for a higher level abstraction.
|
||||||
|
/// The widgets library, for example, introduces the [View] widget, which
|
||||||
|
/// registers its [RenderView] and [PipelineOwner] with this binding.
|
||||||
mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable {
|
mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable {
|
||||||
@override
|
@override
|
||||||
void initInstances() {
|
void initInstances() {
|
||||||
super.initInstances();
|
super.initInstances();
|
||||||
_instance = this;
|
_instance = this;
|
||||||
_pipelineOwner = PipelineOwner(
|
_rootPipelineOwner = createRootPipelineOwner();
|
||||||
onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,
|
|
||||||
onSemanticsUpdate: _handleSemanticsUpdate,
|
|
||||||
onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,
|
|
||||||
);
|
|
||||||
platformDispatcher
|
platformDispatcher
|
||||||
..onMetricsChanged = handleMetricsChanged
|
..onMetricsChanged = handleMetricsChanged
|
||||||
..onTextScaleFactorChanged = handleTextScaleFactorChanged
|
..onTextScaleFactorChanged = handleTextScaleFactorChanged
|
||||||
..onPlatformBrightnessChanged = handlePlatformBrightnessChanged;
|
..onPlatformBrightnessChanged = handlePlatformBrightnessChanged;
|
||||||
initRenderView();
|
|
||||||
addPersistentFrameCallback(_handlePersistentFrameCallback);
|
addPersistentFrameCallback(_handlePersistentFrameCallback);
|
||||||
initMouseTracker();
|
initMouseTracker();
|
||||||
if (kIsWeb) {
|
if (kIsWeb) {
|
||||||
addPostFrameCallback(_handleWebFirstFrame);
|
addPostFrameCallback(_handleWebFirstFrame);
|
||||||
}
|
}
|
||||||
_pipelineOwner.attach(_manifold);
|
rootPipelineOwner.attach(_manifold);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The current [RendererBinding], if one has been created.
|
/// The current [RendererBinding], if one has been created.
|
||||||
@ -108,9 +113,8 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
|
|||||||
registerServiceExtension(
|
registerServiceExtension(
|
||||||
name: RenderingServiceExtensions.debugDumpLayerTree.name,
|
name: RenderingServiceExtensions.debugDumpLayerTree.name,
|
||||||
callback: (Map<String, String> parameters) async {
|
callback: (Map<String, String> parameters) async {
|
||||||
final String data = RendererBinding.instance.renderView.debugLayer?.toStringDeep() ?? 'Layer tree unavailable.';
|
|
||||||
return <String, Object>{
|
return <String, Object>{
|
||||||
'data': data,
|
'data': _debugCollectLayerTrees(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -155,9 +159,8 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
|
|||||||
registerServiceExtension(
|
registerServiceExtension(
|
||||||
name: RenderingServiceExtensions.debugDumpRenderTree.name,
|
name: RenderingServiceExtensions.debugDumpRenderTree.name,
|
||||||
callback: (Map<String, String> parameters) async {
|
callback: (Map<String, String> parameters) async {
|
||||||
final String data = RendererBinding.instance.renderView.toStringDeep();
|
|
||||||
return <String, Object>{
|
return <String, Object>{
|
||||||
'data': data,
|
'data': _debugCollectRenderTrees(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -165,7 +168,7 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
|
|||||||
name: RenderingServiceExtensions.debugDumpSemanticsTreeInTraversalOrder.name,
|
name: RenderingServiceExtensions.debugDumpSemanticsTreeInTraversalOrder.name,
|
||||||
callback: (Map<String, String> parameters) async {
|
callback: (Map<String, String> parameters) async {
|
||||||
return <String, Object>{
|
return <String, Object>{
|
||||||
'data': _generateSemanticsTree(DebugSemanticsDumpOrder.traversalOrder),
|
'data': _debugCollectSemanticsTrees(DebugSemanticsDumpOrder.traversalOrder),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -173,7 +176,7 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
|
|||||||
name: RenderingServiceExtensions.debugDumpSemanticsTreeInInverseHitTestOrder.name,
|
name: RenderingServiceExtensions.debugDumpSemanticsTreeInInverseHitTestOrder.name,
|
||||||
callback: (Map<String, String> parameters) async {
|
callback: (Map<String, String> parameters) async {
|
||||||
return <String, Object>{
|
return <String, Object>{
|
||||||
'data': _generateSemanticsTree(DebugSemanticsDumpOrder.inverseHitTest),
|
'data': _debugCollectSemanticsTrees(DebugSemanticsDumpOrder.inverseHitTest),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -200,38 +203,156 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
|
|||||||
|
|
||||||
late final PipelineManifold _manifold = _BindingPipelineManifold(this);
|
late final PipelineManifold _manifold = _BindingPipelineManifold(this);
|
||||||
|
|
||||||
/// Creates a [RenderView] object to be the root of the
|
|
||||||
/// [RenderObject] rendering tree, and initializes it so that it
|
|
||||||
/// will be rendered when the next frame is requested.
|
|
||||||
///
|
|
||||||
/// Called automatically when the binding is created.
|
|
||||||
void initRenderView() {
|
|
||||||
assert(!_debugIsRenderViewInitialized);
|
|
||||||
assert(() {
|
|
||||||
_debugIsRenderViewInitialized = true;
|
|
||||||
return true;
|
|
||||||
}());
|
|
||||||
renderView = RenderView(configuration: createViewConfiguration(), view: platformDispatcher.implicitView!);
|
|
||||||
renderView.prepareInitialFrame();
|
|
||||||
}
|
|
||||||
bool _debugIsRenderViewInitialized = false;
|
|
||||||
|
|
||||||
/// The object that manages state about currently connected mice, for hover
|
/// The object that manages state about currently connected mice, for hover
|
||||||
/// notification.
|
/// notification.
|
||||||
MouseTracker get mouseTracker => _mouseTracker!;
|
MouseTracker get mouseTracker => _mouseTracker!;
|
||||||
MouseTracker? _mouseTracker;
|
MouseTracker? _mouseTracker;
|
||||||
|
|
||||||
/// The render tree's owner, which maintains dirty state for layout,
|
/// Deprecated. Will be removed in a future version of Flutter.
|
||||||
/// composite, paint, and accessibility semantics.
|
///
|
||||||
PipelineOwner get pipelineOwner => _pipelineOwner;
|
/// This is typically the owner of the render tree bootstrapped by [runApp]
|
||||||
late PipelineOwner _pipelineOwner;
|
/// and rooted in [renderView]. It maintains dirty state for layout,
|
||||||
|
/// composite, paint, and accessibility semantics for that tree.
|
||||||
|
///
|
||||||
|
/// However, by default, the [pipelineOwner] does not participate in frame
|
||||||
|
/// production because it is not automatically attached to the
|
||||||
|
/// [rootPipelineOwner] or any of its descendants. It is also not
|
||||||
|
/// automatically associated with the [renderView]. This is left as a
|
||||||
|
/// responsibility for a higher level abstraction. The [WidgetsBinding], for
|
||||||
|
/// example, wires this up in [WidgetsBinding.wrapWithDefaultView], which is
|
||||||
|
/// called indirectly from [runApp].
|
||||||
|
///
|
||||||
|
/// Apps, that don't use the [WidgetsBinding] or don't call [runApp] (or
|
||||||
|
/// [WidgetsBinding.wrapWithDefaultView]) must manually add this pipeline owner
|
||||||
|
/// to the pipeline owner tree rooted at [rootPipelineOwner] and assign a
|
||||||
|
/// [RenderView] to it if the they want to use this deprecated property.
|
||||||
|
///
|
||||||
|
/// Instead of accessing this deprecated property, consider interacting with
|
||||||
|
/// the root of the [PipelineOwner] tree (exposed in [rootPipelineOwner]) or
|
||||||
|
/// instead of accessing the [SemanticsOwner] of any [PipelineOwner] consider
|
||||||
|
/// interacting with the [SemanticsBinding] (exposed via
|
||||||
|
/// [SemanticsBinding.instance]) directly.
|
||||||
|
@Deprecated(
|
||||||
|
'Interact with the pipelineOwner tree rooted at RendererBinding.rootPipelineOwner instead. '
|
||||||
|
'Or instead of accessing the SemanticsOwner of any PipelineOwner interact with the SemanticsBinding directly. '
|
||||||
|
'This feature was deprecated after v3.10.0-12.0.pre.'
|
||||||
|
)
|
||||||
|
late final PipelineOwner pipelineOwner = PipelineOwner(
|
||||||
|
onSemanticsOwnerCreated: () {
|
||||||
|
(pipelineOwner.rootNode as RenderView?)?.scheduleInitialSemantics();
|
||||||
|
},
|
||||||
|
onSemanticsUpdate: (ui.SemanticsUpdate update) {
|
||||||
|
(pipelineOwner.rootNode as RenderView?)?.updateSemantics(update);
|
||||||
|
},
|
||||||
|
onSemanticsOwnerDisposed: () {
|
||||||
|
(pipelineOwner.rootNode as RenderView?)?.clearSemantics();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
/// The render tree that's attached to the output surface.
|
/// Deprecated. Will be removed in a future version of Flutter.
|
||||||
RenderView get renderView => _pipelineOwner.rootNode! as RenderView;
|
///
|
||||||
/// Sets the given [RenderView] object (which must not be null), and its tree, to
|
/// This is typically the root of the render tree bootstrapped by [runApp].
|
||||||
/// be the new render tree to display. The previous tree, if any, is detached.
|
///
|
||||||
set renderView(RenderView value) {
|
/// However, by default this render view is not associated with any
|
||||||
_pipelineOwner.rootNode = value;
|
/// [PipelineOwner] and therefore isn't considered during frame production.
|
||||||
|
/// It is also not registered with this binding via [addRenderView].
|
||||||
|
/// Wiring this up is left as a responsibility for a higher level. The
|
||||||
|
/// [WidgetsBinding], for example, sets this up in
|
||||||
|
/// [WidgetsBinding.wrapWithDefaultView], which is called indirectly from
|
||||||
|
/// [runApp].
|
||||||
|
///
|
||||||
|
/// Apps that don't use the [WidgetsBinding] or don't call [runApp] (or
|
||||||
|
/// [WidgetsBinding.wrapWithDefaultView]) must manually assign a
|
||||||
|
/// [PipelineOwner] to this [RenderView], make sure the pipeline owner is part
|
||||||
|
/// of the pipeline owner tree rooted at [rootPipelineOwner], and call
|
||||||
|
/// [addRenderView] if they want to use this deprecated property.
|
||||||
|
///
|
||||||
|
/// Instead of interacting with this deprecated property, consider using
|
||||||
|
/// [renderViews] instead, which contains all [RenderView]s managed by the
|
||||||
|
/// binding.
|
||||||
|
@Deprecated(
|
||||||
|
'Consider using RendererBinding.renderViews instead as the binding may manage multiple RenderViews. '
|
||||||
|
'This feature was deprecated after v3.10.0-12.0.pre.'
|
||||||
|
)
|
||||||
|
// TODO(goderbauer): When this deprecated property is removed also delete the _ReusableRenderView class.
|
||||||
|
late final RenderView renderView = _ReusableRenderView(
|
||||||
|
view: platformDispatcher.implicitView!,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Creates the [PipelineOwner] that serves as the root of the pipeline owner
|
||||||
|
/// tree ([rootPipelineOwner]).
|
||||||
|
///
|
||||||
|
/// {@template flutter.rendering.createRootPipelineOwner}
|
||||||
|
/// By default, the root pipeline owner is not setup to manage a render tree
|
||||||
|
/// and its [PipelineOwner.rootNode] must not be assigned. If necessary,
|
||||||
|
/// [createRootPipelineOwner] may be overridden to create a root pipeline
|
||||||
|
/// owner configured to manage its own render tree.
|
||||||
|
///
|
||||||
|
/// In typical use, child pipeline owners are added to the root pipeline owner
|
||||||
|
/// (via [PipelineOwner.adoptChild]). Those children typically do each manage
|
||||||
|
/// their own [RenderView] and produce distinct render trees which render
|
||||||
|
/// their content into the [FlutterView] associated with that [RenderView].
|
||||||
|
/// {@endtemplate}
|
||||||
|
PipelineOwner createRootPipelineOwner() {
|
||||||
|
return _DefaultRootPipelineOwner();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The [PipelineOwner] that is the root of the PipelineOwner tree.
|
||||||
|
///
|
||||||
|
/// {@macro flutter.rendering.createRootPipelineOwner}
|
||||||
|
PipelineOwner get rootPipelineOwner => _rootPipelineOwner;
|
||||||
|
late PipelineOwner _rootPipelineOwner;
|
||||||
|
|
||||||
|
/// The [RenderView]s managed by this binding.
|
||||||
|
///
|
||||||
|
/// A [RenderView] is added by [addRenderView] and removed by [removeRenderView].
|
||||||
|
Iterable<RenderView> get renderViews => _viewIdToRenderView.values;
|
||||||
|
final Map<Object, RenderView> _viewIdToRenderView = <Object, RenderView>{};
|
||||||
|
|
||||||
|
/// Adds a [RenderView] to this binding.
|
||||||
|
///
|
||||||
|
/// The binding will interact with the [RenderView] in the following ways:
|
||||||
|
///
|
||||||
|
/// * setting and updating [RenderView.configuration],
|
||||||
|
/// * calling [RenderView.compositeFrame] when it is time to produce a new
|
||||||
|
/// frame, and
|
||||||
|
/// * forwarding relevant pointer events to the [RenderView] for hit testing.
|
||||||
|
///
|
||||||
|
/// To remove a [RenderView] from the binding, call [removeRenderView].
|
||||||
|
void addRenderView(RenderView view) {
|
||||||
|
final Object viewId = view.flutterView.viewId;
|
||||||
|
assert(!_viewIdToRenderView.containsValue(view));
|
||||||
|
assert(!_viewIdToRenderView.containsKey(viewId));
|
||||||
|
_viewIdToRenderView[viewId] = view;
|
||||||
|
view.configuration = createViewConfigurationFor(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a [RenderView] previously added with [addRenderView] from the
|
||||||
|
/// binding.
|
||||||
|
void removeRenderView(RenderView view) {
|
||||||
|
final Object viewId = view.flutterView.viewId;
|
||||||
|
assert(_viewIdToRenderView[viewId] == view);
|
||||||
|
_viewIdToRenderView.remove(viewId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a [ViewConfiguration] configured for the provided [RenderView]
|
||||||
|
/// based on the current environment.
|
||||||
|
///
|
||||||
|
/// This is called during [addRenderView] and also in response to changes to
|
||||||
|
/// the system metrics to update all [renderViews] added to the binding.
|
||||||
|
///
|
||||||
|
/// Bindings can override this method to change what size or device pixel
|
||||||
|
/// ratio the [RenderView] will use. For example, the testing framework uses
|
||||||
|
/// this to force the display into 800x600 when a test is run on the device
|
||||||
|
/// using `flutter run`.
|
||||||
|
@protected
|
||||||
|
ViewConfiguration createViewConfigurationFor(RenderView renderView) {
|
||||||
|
final FlutterView view = renderView.flutterView;
|
||||||
|
final double devicePixelRatio = view.devicePixelRatio;
|
||||||
|
return ViewConfiguration(
|
||||||
|
size: view.physicalSize / devicePixelRatio,
|
||||||
|
devicePixelRatio: devicePixelRatio,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called when the system metrics change.
|
/// Called when the system metrics change.
|
||||||
@ -240,8 +361,12 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
|
|||||||
@protected
|
@protected
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
void handleMetricsChanged() {
|
void handleMetricsChanged() {
|
||||||
renderView.configuration = createViewConfiguration();
|
bool forceFrame = false;
|
||||||
if (renderView.child != null) {
|
for (final RenderView view in renderViews) {
|
||||||
|
forceFrame = forceFrame || view.child != null;
|
||||||
|
view.configuration = createViewConfigurationFor(view);
|
||||||
|
}
|
||||||
|
if (forceFrame) {
|
||||||
scheduleForcedFrame();
|
scheduleForcedFrame();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -288,25 +413,6 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
|
|||||||
@protected
|
@protected
|
||||||
void handlePlatformBrightnessChanged() { }
|
void handlePlatformBrightnessChanged() { }
|
||||||
|
|
||||||
/// Returns a [ViewConfiguration] configured for the [RenderView] based on the
|
|
||||||
/// current environment.
|
|
||||||
///
|
|
||||||
/// This is called during construction and also in response to changes to the
|
|
||||||
/// system metrics.
|
|
||||||
///
|
|
||||||
/// Bindings can override this method to change what size or device pixel
|
|
||||||
/// ratio the [RenderView] will use. For example, the testing framework uses
|
|
||||||
/// this to force the display into 800x600 when a test is run on the device
|
|
||||||
/// using `flutter run`.
|
|
||||||
ViewConfiguration createViewConfiguration() {
|
|
||||||
final FlutterView view = platformDispatcher.implicitView!;
|
|
||||||
final double devicePixelRatio = view.devicePixelRatio;
|
|
||||||
return ViewConfiguration(
|
|
||||||
size: view.physicalSize / devicePixelRatio,
|
|
||||||
devicePixelRatio: devicePixelRatio,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a [MouseTracker] which manages state about currently connected
|
/// Creates a [MouseTracker] which manages state about currently connected
|
||||||
/// mice, for hover notification.
|
/// mice, for hover notification.
|
||||||
///
|
///
|
||||||
@ -335,19 +441,10 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void performSemanticsAction(SemanticsActionEvent action) {
|
void performSemanticsAction(SemanticsActionEvent action) {
|
||||||
_pipelineOwner.semanticsOwner?.performAction(action.nodeId, action.type, action.arguments);
|
// Due to the asynchronicity in some screen readers (they may not have
|
||||||
}
|
// processed the latest semantics update yet) this code is more forgiving
|
||||||
|
// and actions for views/nodes that no longer exist are gracefully ignored.
|
||||||
void _handleSemanticsOwnerCreated() {
|
_viewIdToRenderView[action.viewId]?.owner?.semanticsOwner?.performAction(action.nodeId, action.type, action.arguments);
|
||||||
renderView.scheduleInitialSemantics();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleSemanticsUpdate(ui.SemanticsUpdate update) {
|
|
||||||
renderView.updateSemantics(update);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleSemanticsOwnerDisposed() {
|
|
||||||
renderView.clearSemantics();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleWebFirstFrame(Duration _) {
|
void _handleWebFirstFrame(Duration _) {
|
||||||
@ -491,12 +588,14 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
|
|||||||
// When editing the above, also update widgets/binding.dart's copy.
|
// When editing the above, also update widgets/binding.dart's copy.
|
||||||
@protected
|
@protected
|
||||||
void drawFrame() {
|
void drawFrame() {
|
||||||
pipelineOwner.flushLayout();
|
rootPipelineOwner.flushLayout();
|
||||||
pipelineOwner.flushCompositingBits();
|
rootPipelineOwner.flushCompositingBits();
|
||||||
pipelineOwner.flushPaint();
|
rootPipelineOwner.flushPaint();
|
||||||
if (sendFramesToEngine) {
|
if (sendFramesToEngine) {
|
||||||
|
for (final RenderView renderView in renderViews) {
|
||||||
renderView.compositeFrame(); // this sends the bits to the GPU
|
renderView.compositeFrame(); // this sends the bits to the GPU
|
||||||
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
|
}
|
||||||
|
rootPipelineOwner.flushSemantics(); // this sends the semantics to the OS.
|
||||||
_firstFrameSent = true;
|
_firstFrameSent = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -509,7 +608,9 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
|
|||||||
FlutterTimeline.startSync('Preparing Hot Reload (layout)');
|
FlutterTimeline.startSync('Preparing Hot Reload (layout)');
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
for (final RenderView renderView in renderViews) {
|
||||||
renderView.reassemble();
|
renderView.reassemble();
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (!kReleaseMode) {
|
if (!kReleaseMode) {
|
||||||
FlutterTimeline.finishSync();
|
FlutterTimeline.finishSync();
|
||||||
@ -520,18 +621,9 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
|
|||||||
await endOfFrame;
|
await endOfFrame;
|
||||||
}
|
}
|
||||||
|
|
||||||
late final int _implicitViewId = platformDispatcher.implicitView!.viewId;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void hitTestInView(HitTestResult result, Offset position, int viewId) {
|
void hitTestInView(HitTestResult result, Offset position, int viewId) {
|
||||||
// Currently Flutter only supports one view, the implicit view `renderView`.
|
_viewIdToRenderView[viewId]?.hitTest(result, position: position);
|
||||||
// TODO(dkwingsmt): After Flutter supports multi-view, look up the correct
|
|
||||||
// render view for the ID.
|
|
||||||
// https://github.com/flutter/flutter/issues/121573
|
|
||||||
assert(viewId == _implicitViewId,
|
|
||||||
'Unexpected view ID $viewId (expecting implicit view ID $_implicitViewId)');
|
|
||||||
assert(viewId == renderView.flutterView.viewId);
|
|
||||||
renderView.hitTest(result, position: position);
|
|
||||||
super.hitTestInView(result, position, viewId);
|
super.hitTestInView(result, position, viewId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -541,40 +633,93 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
|
|||||||
child.markNeedsPaint();
|
child.markNeedsPaint();
|
||||||
child.visitChildren(visitor);
|
child.visitChildren(visitor);
|
||||||
};
|
};
|
||||||
instance.renderView.visitChildren(visitor);
|
for (final RenderView renderView in renderViews) {
|
||||||
|
renderView.visitChildren(visitor);
|
||||||
|
}
|
||||||
return endOfFrame;
|
return endOfFrame;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prints a textual representation of the entire render tree.
|
String _debugCollectRenderTrees() {
|
||||||
|
if (RendererBinding.instance.renderViews.isEmpty) {
|
||||||
|
return 'No render tree root was added to the binding.';
|
||||||
|
}
|
||||||
|
return <String>[
|
||||||
|
for (final RenderView renderView in RendererBinding.instance.renderViews)
|
||||||
|
renderView.toStringDeep(),
|
||||||
|
].join('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prints a textual representation of the render trees.
|
||||||
|
///
|
||||||
|
/// {@template flutter.rendering.debugDumpRenderTree}
|
||||||
|
/// It prints the trees associated with every [RenderView] in
|
||||||
|
/// [RendererBinding.renderView], separated by two blank lines.
|
||||||
|
/// {@endtemplate}
|
||||||
void debugDumpRenderTree() {
|
void debugDumpRenderTree() {
|
||||||
debugPrint(RendererBinding.instance.renderView.toStringDeep());
|
debugPrint(_debugCollectRenderTrees());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prints a textual representation of the entire layer tree.
|
String _debugCollectLayerTrees() {
|
||||||
|
if (RendererBinding.instance.renderViews.isEmpty) {
|
||||||
|
return 'No render tree root was added to the binding.';
|
||||||
|
}
|
||||||
|
return <String>[
|
||||||
|
for (final RenderView renderView in RendererBinding.instance.renderViews)
|
||||||
|
renderView.debugLayer?.toStringDeep() ?? 'Layer tree unavailable for $renderView.',
|
||||||
|
].join('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prints a textual representation of the layer trees.
|
||||||
|
///
|
||||||
|
/// {@macro flutter.rendering.debugDumpRenderTree}
|
||||||
void debugDumpLayerTree() {
|
void debugDumpLayerTree() {
|
||||||
debugPrint(RendererBinding.instance.renderView.debugLayer?.toStringDeep());
|
debugPrint(_debugCollectLayerTrees());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prints a textual representation of the entire semantics tree.
|
String _debugCollectSemanticsTrees(DebugSemanticsDumpOrder childOrder) {
|
||||||
/// This will only work if there is a semantics client attached.
|
if (RendererBinding.instance.renderViews.isEmpty) {
|
||||||
/// Otherwise, a notice that no semantics are available will be printed.
|
return 'No render tree root was added to the binding.';
|
||||||
|
}
|
||||||
|
const String explanation = 'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n'
|
||||||
|
'Usually, platforms only ask for semantics when assistive technologies (like screen readers) are running.\n'
|
||||||
|
'To generate semantics, try turning on an assistive technology (like VoiceOver or TalkBack) on your device.';
|
||||||
|
final List<String> trees = <String>[];
|
||||||
|
bool printedExplanation = false;
|
||||||
|
for (final RenderView renderView in RendererBinding.instance.renderViews) {
|
||||||
|
final String? tree = renderView.debugSemantics?.toStringDeep(childOrder: childOrder);
|
||||||
|
if (tree != null) {
|
||||||
|
trees.add(tree);
|
||||||
|
} else {
|
||||||
|
String message = 'Semantics not generated for $renderView.';
|
||||||
|
if (!printedExplanation) {
|
||||||
|
printedExplanation = true;
|
||||||
|
message = '$message\n$explanation';
|
||||||
|
}
|
||||||
|
trees.add(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return trees.join('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prints a textual representation of the semantics trees.
|
||||||
|
///
|
||||||
|
/// {@macro flutter.rendering.debugDumpRenderTree}
|
||||||
|
///
|
||||||
|
/// Semantics trees are only constructed when semantics are enabled (see
|
||||||
|
/// [SemanticsBinding.semanticsEnabled]). If a semantics tree is not available,
|
||||||
|
/// a notice about the missing semantics tree is printed instead.
|
||||||
///
|
///
|
||||||
/// The order in which the children of a [SemanticsNode] will be printed is
|
/// The order in which the children of a [SemanticsNode] will be printed is
|
||||||
/// controlled by the [childOrder] parameter.
|
/// controlled by the [childOrder] parameter.
|
||||||
void debugDumpSemanticsTree([DebugSemanticsDumpOrder childOrder = DebugSemanticsDumpOrder.traversalOrder]) {
|
void debugDumpSemanticsTree([DebugSemanticsDumpOrder childOrder = DebugSemanticsDumpOrder.traversalOrder]) {
|
||||||
debugPrint(_generateSemanticsTree(childOrder));
|
debugPrint(_debugCollectSemanticsTrees(childOrder));
|
||||||
}
|
}
|
||||||
|
|
||||||
String _generateSemanticsTree(DebugSemanticsDumpOrder childOrder) {
|
/// Prints a textual representation of the [PipelineOwner] tree rooted at
|
||||||
final String? tree = RendererBinding.instance.renderView.debugSemantics?.toStringDeep(childOrder: childOrder);
|
/// [RendererBinding.rootPipelineOwner].
|
||||||
if (tree != null) {
|
void debugDumpPipelineOwnerTree() {
|
||||||
return tree;
|
debugPrint(RendererBinding.instance.rootPipelineOwner.toStringDeep());
|
||||||
}
|
|
||||||
return 'Semantics not generated.\n'
|
|
||||||
'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n'
|
|
||||||
'Usually, platforms only ask for semantics when assistive technologies (like screen readers) are running.\n'
|
|
||||||
'To generate semantics, try turning on an assistive technology (like VoiceOver or TalkBack) on your device.';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A concrete binding for applications that use the Rendering framework
|
/// A concrete binding for applications that use the Rendering framework
|
||||||
@ -595,18 +740,17 @@ String _generateSemanticsTree(DebugSemanticsDumpOrder childOrder) {
|
|||||||
/// rendering layer directly. If you are writing to a higher-level
|
/// rendering layer directly. If you are writing to a higher-level
|
||||||
/// library, such as the Flutter Widgets library, then you would use
|
/// library, such as the Flutter Widgets library, then you would use
|
||||||
/// that layer's binding (see [WidgetsFlutterBinding]).
|
/// that layer's binding (see [WidgetsFlutterBinding]).
|
||||||
|
///
|
||||||
|
/// The [RenderingFlutterBinding] can manage multiple render trees. Each render
|
||||||
|
/// tree is rooted in a [RenderView] that must be added to the binding via
|
||||||
|
/// [addRenderView] to be consider during frame production, hit testing, etc.
|
||||||
|
/// Furthermore, the render tree must be managed by a [PipelineOwner] that is
|
||||||
|
/// part of the pipeline owner tree rooted at [rootPipelineOwner].
|
||||||
|
///
|
||||||
|
/// Adding [PipelineOwner]s and [RenderView]s to this binding in the way
|
||||||
|
/// described above is left as a responsibility for a higher level abstraction.
|
||||||
|
/// The binding does not own any [RenderView]s directly.
|
||||||
class RenderingFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, SemanticsBinding, PaintingBinding, RendererBinding {
|
class RenderingFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, SemanticsBinding, PaintingBinding, RendererBinding {
|
||||||
/// Creates a binding for the rendering layer.
|
|
||||||
///
|
|
||||||
/// The `root` render box is attached directly to the [renderView] and is
|
|
||||||
/// given constraints that require it to fill the window.
|
|
||||||
///
|
|
||||||
/// This binding does not automatically schedule any frames. Callers are
|
|
||||||
/// responsible for deciding when to first call [scheduleFrame].
|
|
||||||
RenderingFlutterBinding({ RenderBox? root }) {
|
|
||||||
renderView.child = root;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns an instance of the binding that implements
|
/// Returns an instance of the binding that implements
|
||||||
/// [RendererBinding]. If no binding has yet been initialized, the
|
/// [RendererBinding]. If no binding has yet been initialized, the
|
||||||
/// [RenderingFlutterBinding] class is used to create and initialize
|
/// [RenderingFlutterBinding] class is used to create and initialize
|
||||||
@ -645,3 +789,82 @@ class _BindingPipelineManifold extends ChangeNotifier implements PipelineManifol
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A [PipelineOwner] that cannot have a root node.
|
||||||
|
class _DefaultRootPipelineOwner extends PipelineOwner {
|
||||||
|
_DefaultRootPipelineOwner() : super(onSemanticsUpdate: _onSemanticsUpdate);
|
||||||
|
|
||||||
|
@override
|
||||||
|
set rootNode(RenderObject? _) {
|
||||||
|
assert(() {
|
||||||
|
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||||
|
ErrorSummary(
|
||||||
|
'Cannot set a rootNode on the default root pipeline owner.',
|
||||||
|
),
|
||||||
|
ErrorDescription(
|
||||||
|
'By default, the RendererBinding.rootPipelineOwner is not configured '
|
||||||
|
'to manage a root node because this pipeline owner does not define a '
|
||||||
|
'proper onSemanticsUpdate callback to handle semantics for that node.',
|
||||||
|
),
|
||||||
|
ErrorHint(
|
||||||
|
'Typically, the root pipeline owner does not manage a root node. '
|
||||||
|
'Instead, properly configured child pipeline owners (which do manage '
|
||||||
|
'root nodes) are added to it. Alternatively, if you do want to set a '
|
||||||
|
'root node for the root pipeline owner, override '
|
||||||
|
'RendererBinding.createRootPipelineOwner to create a '
|
||||||
|
'pipeline owner that is configured to properly handle semantics for '
|
||||||
|
'the provided root node.'
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _onSemanticsUpdate(ui.SemanticsUpdate _) {
|
||||||
|
// Neve called because we don't have a root node.
|
||||||
|
assert(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prior to multi view support, the [RendererBinding] would own a long-lived
|
||||||
|
// [RenderView], that was never disposed (see [RendererBinding.renderView]).
|
||||||
|
// With multi view support, the [RendererBinding] no longer owns a [RenderView]
|
||||||
|
// and instead higher level abstractions (like the [View] widget) can add/remove
|
||||||
|
// multiple [RenderView]s to the binding as needed. When the [View] widget is no
|
||||||
|
// longer needed, it expects to dispose its [RenderView].
|
||||||
|
//
|
||||||
|
// This special version of a [RenderView] now exists as a bridge between those
|
||||||
|
// worlds to continue supporting the [RendererBinding.renderView] property
|
||||||
|
// through its deprecation period. Per the property's contract, it is supposed
|
||||||
|
// to be long-lived, but it is also managed by a [View] widget (introduced by
|
||||||
|
// [WidgetsBinding.wrapWithDefaultView]), that expects to dispose its render
|
||||||
|
// object at the end of the widget's life time. This special version now
|
||||||
|
// implements logic to reset the [RenderView] when it is "disposed" so it can be
|
||||||
|
// reused by another [View] widget.
|
||||||
|
//
|
||||||
|
// Once the deprecated [RendererBinding.renderView] property is removed, this
|
||||||
|
// class is no longer necessary.
|
||||||
|
class _ReusableRenderView extends RenderView {
|
||||||
|
_ReusableRenderView({required super.view});
|
||||||
|
|
||||||
|
bool _initialFramePrepared = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void prepareInitialFrame() {
|
||||||
|
if (_initialFramePrepared) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
super.prepareInitialFrame();
|
||||||
|
_initialFramePrepared = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void scheduleInitialSemantics() {
|
||||||
|
clearSemantics();
|
||||||
|
super.scheduleInitialSemantics();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() { // ignore: must_call_super
|
||||||
|
child = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -870,7 +870,7 @@ class _LocalSemanticsHandle implements SemanticsHandle {
|
|||||||
/// without tying it to a specific binding implementation. All [PipelineOwner]s
|
/// without tying it to a specific binding implementation. All [PipelineOwner]s
|
||||||
/// in a given tree must be attached to the same [PipelineManifold]. This
|
/// in a given tree must be attached to the same [PipelineManifold]. This
|
||||||
/// happens automatically during [adoptChild].
|
/// happens automatically during [adoptChild].
|
||||||
class PipelineOwner {
|
class PipelineOwner with DiagnosticableTreeMixin {
|
||||||
/// Creates a pipeline owner.
|
/// Creates a pipeline owner.
|
||||||
///
|
///
|
||||||
/// Typically created by the binding (e.g., [RendererBinding]), but can be
|
/// Typically created by the binding (e.g., [RendererBinding]), but can be
|
||||||
@ -984,7 +984,7 @@ class PipelineOwner {
|
|||||||
return true;
|
return true;
|
||||||
}());
|
}());
|
||||||
FlutterTimeline.startSync(
|
FlutterTimeline.startSync(
|
||||||
'LAYOUT',
|
'LAYOUT$_debugRootSuffixForTimelineEventNames',
|
||||||
arguments: debugTimelineArguments,
|
arguments: debugTimelineArguments,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1071,7 +1071,7 @@ class PipelineOwner {
|
|||||||
/// [flushPaint].
|
/// [flushPaint].
|
||||||
void flushCompositingBits() {
|
void flushCompositingBits() {
|
||||||
if (!kReleaseMode) {
|
if (!kReleaseMode) {
|
||||||
FlutterTimeline.startSync('UPDATING COMPOSITING BITS');
|
FlutterTimeline.startSync('UPDATING COMPOSITING BITS$_debugRootSuffixForTimelineEventNames');
|
||||||
}
|
}
|
||||||
_nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
|
_nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
|
||||||
for (final RenderObject node in _nodesNeedingCompositingBitsUpdate) {
|
for (final RenderObject node in _nodesNeedingCompositingBitsUpdate) {
|
||||||
@ -1120,7 +1120,7 @@ class PipelineOwner {
|
|||||||
return true;
|
return true;
|
||||||
}());
|
}());
|
||||||
FlutterTimeline.startSync(
|
FlutterTimeline.startSync(
|
||||||
'PAINT',
|
'PAINT$_debugRootSuffixForTimelineEventNames',
|
||||||
arguments: debugTimelineArguments,
|
arguments: debugTimelineArguments,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1247,7 +1247,7 @@ class PipelineOwner {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!kReleaseMode) {
|
if (!kReleaseMode) {
|
||||||
FlutterTimeline.startSync('SEMANTICS');
|
FlutterTimeline.startSync('SEMANTICS$_debugRootSuffixForTimelineEventNames');
|
||||||
}
|
}
|
||||||
assert(_semanticsOwner != null);
|
assert(_semanticsOwner != null);
|
||||||
assert(() {
|
assert(() {
|
||||||
@ -1279,6 +1279,20 @@ class PipelineOwner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<DiagnosticsNode> debugDescribeChildren() {
|
||||||
|
return <DiagnosticsNode>[
|
||||||
|
for (final PipelineOwner child in _children)
|
||||||
|
child.toDiagnosticsNode(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties.add(DiagnosticsProperty<RenderObject>('rootNode', rootNode, defaultValue: null));
|
||||||
|
}
|
||||||
|
|
||||||
// TREE MANAGEMENT
|
// TREE MANAGEMENT
|
||||||
|
|
||||||
final Set<PipelineOwner> _children = <PipelineOwner>{};
|
final Set<PipelineOwner> _children = <PipelineOwner>{};
|
||||||
@ -1290,6 +1304,8 @@ class PipelineOwner {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get _debugRootSuffixForTimelineEventNames => _debugParent == null ? ' (root)' : '';
|
||||||
|
|
||||||
/// Mark this [PipelineOwner] as attached to the given [PipelineManifold].
|
/// Mark this [PipelineOwner] as attached to the given [PipelineManifold].
|
||||||
///
|
///
|
||||||
/// Typically, this is only called directly on the root [PipelineOwner].
|
/// Typically, this is only called directly on the root [PipelineOwner].
|
||||||
@ -1315,7 +1331,9 @@ class PipelineOwner {
|
|||||||
assert(_manifold != null);
|
assert(_manifold != null);
|
||||||
_manifold!.removeListener(_updateSemanticsOwner);
|
_manifold!.removeListener(_updateSemanticsOwner);
|
||||||
_manifold = null;
|
_manifold = null;
|
||||||
_updateSemanticsOwner();
|
// Not updating the semantics owner here to not disrupt any of its clients
|
||||||
|
// in case we get re-attached. If necessary, semantics owner will be updated
|
||||||
|
// in "attach", or disposed in "dispose", if not reattached.
|
||||||
|
|
||||||
for (final PipelineOwner child in _children) {
|
for (final PipelineOwner child in _children) {
|
||||||
child.detach();
|
child.detach();
|
||||||
@ -1351,7 +1369,9 @@ class PipelineOwner {
|
|||||||
assert(!_children.contains(child));
|
assert(!_children.contains(child));
|
||||||
assert(_debugAllowChildListModifications, 'Cannot modify child list after layout.');
|
assert(_debugAllowChildListModifications, 'Cannot modify child list after layout.');
|
||||||
_children.add(child);
|
_children.add(child);
|
||||||
assert(_debugSetParent(child, this));
|
if (!kReleaseMode) {
|
||||||
|
_debugSetParent(child, this);
|
||||||
|
}
|
||||||
if (_manifold != null) {
|
if (_manifold != null) {
|
||||||
child.attach(_manifold!);
|
child.attach(_manifold!);
|
||||||
}
|
}
|
||||||
@ -1369,7 +1389,9 @@ class PipelineOwner {
|
|||||||
assert(_children.contains(child));
|
assert(_children.contains(child));
|
||||||
assert(_debugAllowChildListModifications, 'Cannot modify child list after layout.');
|
assert(_debugAllowChildListModifications, 'Cannot modify child list after layout.');
|
||||||
_children.remove(child);
|
_children.remove(child);
|
||||||
assert(_debugSetParent(child, null));
|
if (!kReleaseMode) {
|
||||||
|
_debugSetParent(child, null);
|
||||||
|
}
|
||||||
if (_manifold != null) {
|
if (_manifold != null) {
|
||||||
child.detach();
|
child.detach();
|
||||||
}
|
}
|
||||||
@ -1384,6 +1406,26 @@ class PipelineOwner {
|
|||||||
void visitChildren(PipelineOwnerVisitor visitor) {
|
void visitChildren(PipelineOwnerVisitor visitor) {
|
||||||
_children.forEach(visitor);
|
_children.forEach(visitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Release any resources held by this pipeline owner.
|
||||||
|
///
|
||||||
|
/// Prior to calling this method the pipeline owner must be removed from the
|
||||||
|
/// pipeline owner tree, i.e. it must have neither a parent nor any children
|
||||||
|
/// (see [dropChild]). It also must be [detach]ed from any [PipelineManifold].
|
||||||
|
///
|
||||||
|
/// The object is no longer usable after calling dispose.
|
||||||
|
void dispose() {
|
||||||
|
assert(_children.isEmpty);
|
||||||
|
assert(rootNode == null);
|
||||||
|
assert(_manifold == null);
|
||||||
|
assert(_debugParent == null);
|
||||||
|
_semanticsOwner?.dispose();
|
||||||
|
_semanticsOwner = null;
|
||||||
|
_nodesNeedingLayout.clear();
|
||||||
|
_nodesNeedingCompositingBitsUpdate.clear();
|
||||||
|
_nodesNeedingPaint.clear();
|
||||||
|
_nodesNeedingSemantics.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Signature for the callback to [PipelineOwner.visitChildren].
|
/// Signature for the callback to [PipelineOwner.visitChildren].
|
||||||
@ -3919,7 +3961,6 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
|
|||||||
/// This mixin is typically used to implement render objects created
|
/// This mixin is typically used to implement render objects created
|
||||||
/// in a [SingleChildRenderObjectWidget].
|
/// in a [SingleChildRenderObjectWidget].
|
||||||
mixin RenderObjectWithChildMixin<ChildType extends RenderObject> on RenderObject {
|
mixin RenderObjectWithChildMixin<ChildType extends RenderObject> on RenderObject {
|
||||||
|
|
||||||
/// Checks whether the given render object has the correct [runtimeType] to be
|
/// Checks whether the given render object has the correct [runtimeType] to be
|
||||||
/// a child of this render object.
|
/// a child of this render object.
|
||||||
///
|
///
|
||||||
|
@ -67,10 +67,14 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
|
|||||||
///
|
///
|
||||||
/// Typically created by the binding (e.g., [RendererBinding]).
|
/// Typically created by the binding (e.g., [RendererBinding]).
|
||||||
///
|
///
|
||||||
/// The [configuration] must not be null.
|
/// Providing a [configuration] is optional, but a configuration must be set
|
||||||
|
/// before calling [prepareInitialFrame]. This decouples creating the
|
||||||
|
/// [RenderView] object from configuring it. Typically, the object is created
|
||||||
|
/// by the [View] widget and configured by the [RendererBinding] when the
|
||||||
|
/// [RenderView] is registered with it by the [View] widget.
|
||||||
RenderView({
|
RenderView({
|
||||||
RenderBox? child,
|
RenderBox? child,
|
||||||
required ViewConfiguration configuration,
|
ViewConfiguration? configuration,
|
||||||
required ui.FlutterView view,
|
required ui.FlutterView view,
|
||||||
}) : _configuration = configuration,
|
}) : _configuration = configuration,
|
||||||
_view = view {
|
_view = view {
|
||||||
@ -82,26 +86,39 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
|
|||||||
Size _size = Size.zero;
|
Size _size = Size.zero;
|
||||||
|
|
||||||
/// The constraints used for the root layout.
|
/// The constraints used for the root layout.
|
||||||
ViewConfiguration get configuration => _configuration;
|
|
||||||
ViewConfiguration _configuration;
|
|
||||||
|
|
||||||
/// The configuration is initially set by the [configuration] argument
|
|
||||||
/// passed to the constructor.
|
|
||||||
///
|
///
|
||||||
/// Always call [prepareInitialFrame] before changing the configuration.
|
/// Typically, this configuration is set by the [RendererBinding], when the
|
||||||
|
/// [RenderView] is registered with it. It will also update the configuration
|
||||||
|
/// if necessary. Therefore, if used in conjunction with the [RendererBinding]
|
||||||
|
/// this property must not be set manually as the [RendererBinding] will just
|
||||||
|
/// override it.
|
||||||
|
///
|
||||||
|
/// For tests that want to change the size of the view, set
|
||||||
|
/// [TestFlutterView.physicalSize] on the appropriate [TestFlutterView]
|
||||||
|
/// (typically [WidgetTester.view]) instead of setting a configuration
|
||||||
|
/// directly on the [RenderView].
|
||||||
|
ViewConfiguration get configuration => _configuration!;
|
||||||
|
ViewConfiguration? _configuration;
|
||||||
set configuration(ViewConfiguration value) {
|
set configuration(ViewConfiguration value) {
|
||||||
if (configuration == value) {
|
if (_configuration == value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final ViewConfiguration oldConfiguration = _configuration;
|
final ViewConfiguration? oldConfiguration = _configuration;
|
||||||
_configuration = value;
|
_configuration = value;
|
||||||
if (oldConfiguration.toMatrix() != _configuration.toMatrix()) {
|
if (_rootTransform == null) {
|
||||||
|
// [prepareInitialFrame] has not been called yet, nothing to do for now.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (oldConfiguration?.toMatrix() != configuration.toMatrix()) {
|
||||||
replaceRootLayer(_updateMatricesAndCreateNewRootLayer());
|
replaceRootLayer(_updateMatricesAndCreateNewRootLayer());
|
||||||
}
|
}
|
||||||
assert(_rootTransform != null);
|
assert(_rootTransform != null);
|
||||||
markNeedsLayout();
|
markNeedsLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether a [configuration] has been set.
|
||||||
|
bool get hasConfiguration => _configuration != null;
|
||||||
|
|
||||||
/// The [FlutterView] into which this [RenderView] will render.
|
/// The [FlutterView] into which this [RenderView] will render.
|
||||||
ui.FlutterView get flutterView => _view;
|
ui.FlutterView get flutterView => _view;
|
||||||
final ui.FlutterView _view;
|
final ui.FlutterView _view;
|
||||||
|
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 glue between the widgets layer and the Flutter engine.
|
||||||
|
///
|
||||||
|
/// The [WidgetsBinding] manages a single [Element] tree rooted at [rootElement].
|
||||||
|
/// Calling [runApp] (which indirectly calls [attachRootWidget]) bootstraps that
|
||||||
|
/// element tree.
|
||||||
|
///
|
||||||
|
/// ## Relationship to render trees
|
||||||
|
///
|
||||||
|
/// Multiple render trees may be associated with the element tree. Those are
|
||||||
|
/// managed by the underlying [RendererBinding].
|
||||||
|
///
|
||||||
|
/// The element tree is segmented into two types of zones: rendering zones and
|
||||||
|
/// non-rendering zones.
|
||||||
|
///
|
||||||
|
/// A rendering zone is a part of the element tree that is backed by a render
|
||||||
|
/// tree and it describes the pixels that are drawn on screen. For elements in
|
||||||
|
/// this zone, [Element.renderObject] never returns null because the elements
|
||||||
|
/// are all associated with [RenderObject]s. Almost all widgets can be placed in
|
||||||
|
/// a rendering zone; notable exceptions are the [View] widget, [ViewCollection]
|
||||||
|
/// widget, and [RootWidget].
|
||||||
|
///
|
||||||
|
/// A non-rendering zone is a part of the element tree that is not backed by a
|
||||||
|
/// render tree. For elements in this zone, [Element.renderObject] returns null
|
||||||
|
/// because the elements are not associated with any [RenderObject]s. Only
|
||||||
|
/// widgets that do not produce a [RenderObject] can be used in this zone
|
||||||
|
/// because there is no render tree to attach the render object to. In other
|
||||||
|
/// words, [RenderObjectWidget]s cannot be used in this zone. Typically, one
|
||||||
|
/// would find [InheritedWidget]s, [View]s, and [ViewCollection]s in this zone
|
||||||
|
/// to inject data across rendering zones into the tree and to organize the
|
||||||
|
/// rendering zones (and by extension their associated render trees) into a
|
||||||
|
/// unified element tree.
|
||||||
|
///
|
||||||
|
/// The root of the element tree at [rootElement] starts a non-rendering zone.
|
||||||
|
/// Within a non-rendering zone, the [View] widget is used to start a rendering
|
||||||
|
/// zone by bootstrapping a render tree. Within a rendering zone, the
|
||||||
|
/// [ViewAnchor] can be used to start a new non-rendering zone.
|
||||||
|
///
|
||||||
|
// TODO(goderbauer): Include an example graph showcasing the different zones.
|
||||||
|
///
|
||||||
|
/// To figure out if an element is in a rendering zone it may walk up the tree
|
||||||
|
/// calling [Element.debugExpectsRenderObjectForSlot] on its ancestors. If it
|
||||||
|
/// reaches an element that returns false, it is in a non-rendering zone. If it
|
||||||
|
/// reaches a [RenderObjectElement] ancestor it is in a rendering zone.
|
||||||
mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
|
mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
|
||||||
@override
|
@override
|
||||||
void initInstances() {
|
void initInstances() {
|
||||||
@ -975,6 +1017,8 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
|
|||||||
Widget wrapWithDefaultView(Widget rootWidget) {
|
Widget wrapWithDefaultView(Widget rootWidget) {
|
||||||
return View(
|
return View(
|
||||||
view: platformDispatcher.implicitView!,
|
view: platformDispatcher.implicitView!,
|
||||||
|
deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: pipelineOwner,
|
||||||
|
deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: renderView,
|
||||||
child: rootWidget,
|
child: rootWidget,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1000,13 +1044,25 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
|
|||||||
/// * [RenderObjectToWidgetAdapter.attachToRenderTree], which inflates a
|
/// * [RenderObjectToWidgetAdapter.attachToRenderTree], which inflates a
|
||||||
/// widget and attaches it to the render tree.
|
/// widget and attaches it to the render tree.
|
||||||
void attachRootWidget(Widget rootWidget) {
|
void attachRootWidget(Widget rootWidget) {
|
||||||
final bool isBootstrapFrame = rootElement == null;
|
attachToBuildOwner(RootWidget(
|
||||||
_readyToProduceFrames = true;
|
|
||||||
_rootElement = RenderObjectToWidgetAdapter<RenderBox>(
|
|
||||||
container: renderView,
|
|
||||||
debugShortDescription: '[root]',
|
debugShortDescription: '[root]',
|
||||||
child: rootWidget,
|
child: rootWidget,
|
||||||
).attachToRenderTree(buildOwner!, rootElement as RenderObjectToWidgetElement<RenderBox>?);
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called by [attachRootWidget] to attach the provided [RootWidget] to the
|
||||||
|
/// [buildOwner].
|
||||||
|
///
|
||||||
|
/// This creates the [rootElement], if necessary, or re-uses an existing one.
|
||||||
|
///
|
||||||
|
/// This method is rarely called directly, but it can be useful in tests to
|
||||||
|
/// restore the element tree to a previous version by providing the
|
||||||
|
/// [RootWidget] of that version (see [WidgetTester.restartAndRestore] for an
|
||||||
|
/// exemplary use case).
|
||||||
|
void attachToBuildOwner(RootWidget widget) {
|
||||||
|
final bool isBootstrapFrame = rootElement == null;
|
||||||
|
_readyToProduceFrames = true;
|
||||||
|
_rootElement = widget.attach(buildOwner!, rootElement as RootElement?);
|
||||||
if (isBootstrapFrame) {
|
if (isBootstrapFrame) {
|
||||||
SchedulerBinding.instance.ensureVisualUpdate();
|
SchedulerBinding.instance.ensureVisualUpdate();
|
||||||
}
|
}
|
||||||
@ -1121,52 +1177,40 @@ void debugDumpApp() {
|
|||||||
debugPrint(_debugDumpAppString());
|
debugPrint(_debugDumpAppString());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A bridge from a [RenderObject] to an [Element] tree.
|
/// A widget for the root of the widget tree.
|
||||||
///
|
///
|
||||||
/// The given container is the [RenderObject] that the [Element] tree should be
|
/// Exposes an [attach] method to attach the widget tree to a [BuildOwner]. That
|
||||||
/// inserted into. It must be a [RenderObject] that implements the
|
/// method also bootstraps the element tree.
|
||||||
/// [RenderObjectWithChildMixin] protocol. The type argument `T` is the kind of
|
|
||||||
/// [RenderObject] that the container expects as its child.
|
|
||||||
///
|
///
|
||||||
/// Used by [runApp] to bootstrap applications.
|
/// Used by [WidgetsBinding.attachRootWidget] (which is indirectly called by
|
||||||
class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWidget {
|
/// [runApp]) to bootstrap applications.
|
||||||
/// Creates a bridge from a [RenderObject] to an [Element] tree.
|
class RootWidget extends Widget {
|
||||||
///
|
/// Creates a [RootWidget].
|
||||||
/// Used by [WidgetsBinding] to attach the root widget to the [RenderView].
|
const RootWidget({
|
||||||
RenderObjectToWidgetAdapter({
|
super.key,
|
||||||
this.child,
|
this.child,
|
||||||
required this.container,
|
|
||||||
this.debugShortDescription,
|
this.debugShortDescription,
|
||||||
}) : super(key: GlobalObjectKey(container));
|
});
|
||||||
|
|
||||||
/// The widget below this widget in the tree.
|
/// The widget below this widget in the tree.
|
||||||
///
|
///
|
||||||
/// {@macro flutter.widgets.ProxyWidget.child}
|
/// {@macro flutter.widgets.ProxyWidget.child}
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
|
|
||||||
/// The [RenderObject] that is the parent of the [Element] created by this widget.
|
|
||||||
final RenderObjectWithChildMixin<T> container;
|
|
||||||
|
|
||||||
/// A short description of this widget used by debugging aids.
|
/// A short description of this widget used by debugging aids.
|
||||||
final String? debugShortDescription;
|
final String? debugShortDescription;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);
|
RootElement createElement() => RootElement(this);
|
||||||
|
|
||||||
@override
|
/// Inflate this widget and attaches it to the provided [BuildOwner].
|
||||||
RenderObjectWithChildMixin<T> createRenderObject(BuildContext context) => container;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void updateRenderObject(BuildContext context, RenderObject renderObject) { }
|
|
||||||
|
|
||||||
/// Inflate this widget and actually set the resulting [RenderObject] as the
|
|
||||||
/// child of [container].
|
|
||||||
///
|
///
|
||||||
/// If `element` is null, this function will create a new element. Otherwise,
|
/// If `element` is null, this function will create a new element. Otherwise,
|
||||||
/// the given element will have an update scheduled to switch to this widget.
|
/// the given element will have an update scheduled to switch to this widget.
|
||||||
///
|
///
|
||||||
/// Used by [runApp] to bootstrap applications.
|
/// Used by [WidgetsBinding.attachToBuildOwner] (which is indirectly called by
|
||||||
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T>? element ]) {
|
/// [runApp]) to bootstrap applications.
|
||||||
|
RootElement attach(BuildOwner owner, [ RootElement? element ]) {
|
||||||
if (element == null) {
|
if (element == null) {
|
||||||
owner.lockState(() {
|
owner.lockState(() {
|
||||||
element = createElement();
|
element = createElement();
|
||||||
@ -1174,7 +1218,7 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi
|
|||||||
element!.assignOwner(owner);
|
element!.assignOwner(owner);
|
||||||
});
|
});
|
||||||
owner.buildScope(element!, () {
|
owner.buildScope(element!, () {
|
||||||
element!.mount(null, null);
|
element!.mount(/* parent */ null, /* slot */ null);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
element._newWidget = this;
|
element._newWidget = this;
|
||||||
@ -1187,28 +1231,22 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi
|
|||||||
String toStringShort() => debugShortDescription ?? super.toStringShort();
|
String toStringShort() => debugShortDescription ?? super.toStringShort();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The root of the element tree that is hosted by a [RenderObject].
|
/// The root of the element tree.
|
||||||
///
|
///
|
||||||
/// This element class is the instantiation of a [RenderObjectToWidgetAdapter]
|
/// This element class is the instantiation of a [RootWidget]. It can be used
|
||||||
/// widget. It can be used only as the root of an [Element] tree (it cannot be
|
/// only as the root of an [Element] tree (it cannot be mounted into another
|
||||||
/// mounted into another [Element]; it's parent must be null).
|
/// [Element]; its parent must be null).
|
||||||
///
|
///
|
||||||
/// In typical usage, it will be instantiated for a [RenderObjectToWidgetAdapter]
|
/// In typical usage, it will be instantiated for a [RootWidget] by calling
|
||||||
/// whose container is the [RenderView] that connects to the Flutter engine. In
|
/// [RootWidget.attach]. In this usage, it is normally instantiated by the
|
||||||
/// this usage, it is normally instantiated by the bootstrapping logic in the
|
/// bootstrapping logic in the [WidgetsFlutterBinding] singleton created by
|
||||||
/// [WidgetsFlutterBinding] singleton created by [runApp].
|
/// [runApp].
|
||||||
class RenderObjectToWidgetElement<T extends RenderObject> extends RenderObjectElement with RootElementMixin {
|
class RootElement extends Element with RootElementMixin {
|
||||||
/// Creates an element that is hosted by a [RenderObject].
|
/// Creates a [RootElement] for the provided [RootWidget].
|
||||||
///
|
RootElement(RootWidget super.widget);
|
||||||
/// The [RenderObject] created by this element is not automatically set as a
|
|
||||||
/// child of the hosting [RenderObject]. To actually attach this element to
|
|
||||||
/// the render tree, call [RenderObjectToWidgetAdapter.attachToRenderTree].
|
|
||||||
RenderObjectToWidgetElement(RenderObjectToWidgetAdapter<T> super.widget);
|
|
||||||
|
|
||||||
Element? _child;
|
Element? _child;
|
||||||
|
|
||||||
static const Object _rootChildSlot = Object();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void visitChildren(ElementVisitor visitor) {
|
void visitChildren(ElementVisitor visitor) {
|
||||||
if (_child != null) {
|
if (_child != null) {
|
||||||
@ -1225,14 +1263,15 @@ class RenderObjectToWidgetElement<T extends RenderObject> extends RenderObjectEl
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void mount(Element? parent, Object? newSlot) {
|
void mount(Element? parent, Object? newSlot) {
|
||||||
assert(parent == null);
|
assert(parent == null); // We are the root!
|
||||||
super.mount(parent, newSlot);
|
super.mount(parent, newSlot);
|
||||||
_rebuild();
|
_rebuild();
|
||||||
assert(_child != null);
|
assert(_child != null);
|
||||||
|
super.performRebuild(); // clears the "dirty" flag
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void update(RenderObjectToWidgetAdapter<T> newWidget) {
|
void update(RootWidget newWidget) {
|
||||||
super.update(newWidget);
|
super.update(newWidget);
|
||||||
assert(widget == newWidget);
|
assert(widget == newWidget);
|
||||||
_rebuild();
|
_rebuild();
|
||||||
@ -1240,25 +1279,24 @@ class RenderObjectToWidgetElement<T extends RenderObject> extends RenderObjectEl
|
|||||||
|
|
||||||
// When we are assigned a new widget, we store it here
|
// When we are assigned a new widget, we store it here
|
||||||
// until we are ready to update to it.
|
// until we are ready to update to it.
|
||||||
Widget? _newWidget;
|
RootWidget? _newWidget;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void performRebuild() {
|
void performRebuild() {
|
||||||
if (_newWidget != null) {
|
if (_newWidget != null) {
|
||||||
// _newWidget can be null if, for instance, we were rebuilt
|
// _newWidget can be null if, for instance, we were rebuilt
|
||||||
// due to a reassemble.
|
// due to a reassemble.
|
||||||
final Widget newWidget = _newWidget!;
|
final RootWidget newWidget = _newWidget!;
|
||||||
_newWidget = null;
|
_newWidget = null;
|
||||||
update(newWidget as RenderObjectToWidgetAdapter<T>);
|
update(newWidget);
|
||||||
}
|
}
|
||||||
super.performRebuild();
|
super.performRebuild();
|
||||||
assert(_newWidget == null);
|
assert(_newWidget == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@pragma('vm:notify-debugger-on-exception')
|
|
||||||
void _rebuild() {
|
void _rebuild() {
|
||||||
try {
|
try {
|
||||||
_child = updateChild(_child, (widget as RenderObjectToWidgetAdapter<T>).child, _rootChildSlot);
|
_child = updateChild(_child, (widget as RootWidget).child, /* slot */ null);
|
||||||
} catch (exception, stack) {
|
} catch (exception, stack) {
|
||||||
final FlutterErrorDetails details = FlutterErrorDetails(
|
final FlutterErrorDetails details = FlutterErrorDetails(
|
||||||
exception: exception,
|
exception: exception,
|
||||||
@ -1267,31 +1305,18 @@ class RenderObjectToWidgetElement<T extends RenderObject> extends RenderObjectEl
|
|||||||
context: ErrorDescription('attaching to the render tree'),
|
context: ErrorDescription('attaching to the render tree'),
|
||||||
);
|
);
|
||||||
FlutterError.reportError(details);
|
FlutterError.reportError(details);
|
||||||
final Widget error = ErrorWidget.builder(details);
|
// No error widget possible here since it wouldn't have a view to render into.
|
||||||
_child = updateChild(null, error, _rootChildSlot);
|
_child = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RenderObjectWithChildMixin<T> get renderObject => super.renderObject as RenderObjectWithChildMixin<T>;
|
bool get debugDoingBuild => false; // This element doesn't have a build phase.
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void insertRenderObjectChild(RenderObject child, Object? slot) {
|
// There is no ancestor RenderObjectElement that the render object could be attached to.
|
||||||
assert(slot == _rootChildSlot);
|
bool debugExpectsRenderObjectForSlot(Object? slot) => false;
|
||||||
assert(renderObject.debugValidateChild(child));
|
|
||||||
renderObject.child = child as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void moveRenderObjectChild(RenderObject child, Object? oldSlot, Object? newSlot) {
|
|
||||||
assert(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void removeRenderObjectChild(RenderObject child, Object? slot) {
|
|
||||||
assert(renderObject.child == child);
|
|
||||||
renderObject.child = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A concrete binding for applications based on the Widgets framework.
|
/// A concrete binding for applications based on the Widgets framework.
|
||||||
|
@ -3452,6 +3452,11 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
|
|||||||
/// If this object is a [RenderObjectElement], the render object is the one at
|
/// If this object is a [RenderObjectElement], the render object is the one at
|
||||||
/// this location in the tree. Otherwise, this getter will walk down the tree
|
/// this location in the tree. Otherwise, this getter will walk down the tree
|
||||||
/// until it finds a [RenderObjectElement].
|
/// until it finds a [RenderObjectElement].
|
||||||
|
///
|
||||||
|
/// Some locations in the tree are not backed by a render object. In those
|
||||||
|
/// cases, this getter returns null. This can happen, if the element is
|
||||||
|
/// located outside of a [View] since only the element subtree rooted in a
|
||||||
|
/// view has a render tree associated with it.
|
||||||
RenderObject? get renderObject {
|
RenderObject? get renderObject {
|
||||||
Element? current = this;
|
Element? current = this;
|
||||||
while (current != null) {
|
while (current != null) {
|
||||||
@ -3460,17 +3465,33 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
|
|||||||
} else if (current is RenderObjectElement) {
|
} else if (current is RenderObjectElement) {
|
||||||
return current.renderObject;
|
return current.renderObject;
|
||||||
} else {
|
} else {
|
||||||
Element? next;
|
current = current.renderObjectAttachingChild;
|
||||||
current.visitChildren((Element child) {
|
|
||||||
assert(next == null); // This verifies that there's only one child.
|
|
||||||
next = child;
|
|
||||||
});
|
|
||||||
current = next;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the child of this [Element] that will insert a [RenderObject] into
|
||||||
|
/// an ancestor of this Element to construct the render tree.
|
||||||
|
///
|
||||||
|
/// Returns null if this Element doesn't have any children who need to attach
|
||||||
|
/// a [RenderObject] to an ancestor of this [Element]. A [RenderObjectElement]
|
||||||
|
/// will therefore return null because its children insert their
|
||||||
|
/// [RenderObject]s into the [RenderObjectElement] itself and not into an
|
||||||
|
/// ancestor of the [RenderObjectElement].
|
||||||
|
///
|
||||||
|
/// Furthermore, this may return null for [Element]s that hoist their own
|
||||||
|
/// independent render tree and do not extend the ancestor render tree.
|
||||||
|
@protected
|
||||||
|
Element? get renderObjectAttachingChild {
|
||||||
|
Element? next;
|
||||||
|
visitChildren((Element child) {
|
||||||
|
assert(next == null); // This verifies that there's only one child.
|
||||||
|
next = child;
|
||||||
|
});
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<DiagnosticsNode> describeMissingAncestor({ required Type expectedAncestorType }) {
|
List<DiagnosticsNode> describeMissingAncestor({ required Type expectedAncestorType }) {
|
||||||
final List<DiagnosticsNode> information = <DiagnosticsNode>[];
|
final List<DiagnosticsNode> information = <DiagnosticsNode>[];
|
||||||
@ -4021,15 +4042,20 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
|
|||||||
assert(_lifecycleState == _ElementLifecycle.active);
|
assert(_lifecycleState == _ElementLifecycle.active);
|
||||||
assert(child._parent == this);
|
assert(child._parent == this);
|
||||||
void visit(Element element) {
|
void visit(Element element) {
|
||||||
element._updateSlot(newSlot);
|
element.updateSlot(newSlot);
|
||||||
if (element is! RenderObjectElement) {
|
final Element? descendant = element.renderObjectAttachingChild;
|
||||||
element.visitChildren(visit);
|
if (descendant != null) {
|
||||||
|
visit(descendant);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
visit(child);
|
visit(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateSlot(Object? newSlot) {
|
/// Called by [updateSlotForChild] when the framework needs to change the slot
|
||||||
|
/// that this [Element] occupies in its ancestor.
|
||||||
|
@protected
|
||||||
|
@mustCallSuper
|
||||||
|
void updateSlot(Object? newSlot) {
|
||||||
assert(_lifecycleState == _ElementLifecycle.active);
|
assert(_lifecycleState == _ElementLifecycle.active);
|
||||||
assert(_parent != null);
|
assert(_parent != null);
|
||||||
assert(_parent!._lifecycleState == _ElementLifecycle.active);
|
assert(_parent!._lifecycleState == _ElementLifecycle.active);
|
||||||
@ -4070,7 +4096,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
|
|||||||
///
|
///
|
||||||
/// The `newSlot` argument specifies the new value for this element's [slot].
|
/// The `newSlot` argument specifies the new value for this element's [slot].
|
||||||
void attachRenderObject(Object? newSlot) {
|
void attachRenderObject(Object? newSlot) {
|
||||||
assert(_slot == null);
|
assert(slot == null);
|
||||||
visitChildren((Element child) {
|
visitChildren((Element child) {
|
||||||
child.attachRenderObject(newSlot);
|
child.attachRenderObject(newSlot);
|
||||||
});
|
});
|
||||||
@ -4143,7 +4169,6 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
|
|||||||
@protected
|
@protected
|
||||||
@pragma('vm:prefer-inline')
|
@pragma('vm:prefer-inline')
|
||||||
Element inflateWidget(Widget newWidget, Object? newSlot) {
|
Element inflateWidget(Widget newWidget, Object? newSlot) {
|
||||||
|
|
||||||
final bool isTimelineTracked = !kReleaseMode && _isProfileBuildsEnabledFor(newWidget);
|
final bool isTimelineTracked = !kReleaseMode && _isProfileBuildsEnabledFor(newWidget);
|
||||||
if (isTimelineTracked) {
|
if (isTimelineTracked) {
|
||||||
Map<String, String>? debugTimelineArguments;
|
Map<String, String>? debugTimelineArguments;
|
||||||
@ -4169,7 +4194,17 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
|
|||||||
_debugCheckForCycles(newChild);
|
_debugCheckForCycles(newChild);
|
||||||
return true;
|
return true;
|
||||||
}());
|
}());
|
||||||
|
try {
|
||||||
newChild._activateWithParent(this, newSlot);
|
newChild._activateWithParent(this, newSlot);
|
||||||
|
} catch (_) {
|
||||||
|
// Attempt to do some clean-up if activation fails to leave tree in a reasonable state.
|
||||||
|
try {
|
||||||
|
deactivateChild(newChild);
|
||||||
|
} catch (_) {
|
||||||
|
// Clean-up failed. Only surface original exception.
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
final Element? updatedChild = updateChild(newChild, newWidget, newSlot);
|
final Element? updatedChild = updateChild(newChild, newWidget, newSlot);
|
||||||
assert(newChild == updatedChild);
|
assert(newChild == updatedChild);
|
||||||
return updatedChild!;
|
return updatedChild!;
|
||||||
@ -4404,6 +4439,33 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
|
|||||||
_lifecycleState = _ElementLifecycle.defunct;
|
_lifecycleState = _ElementLifecycle.defunct;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the child in the provided `slot` (or one of its descendants) must
|
||||||
|
/// insert a [RenderObject] into its ancestor [RenderObjectElement] by calling
|
||||||
|
/// [RenderObjectElement.insertRenderObjectChild] on it.
|
||||||
|
///
|
||||||
|
/// This method is used to define non-rendering zones in the element tree (see
|
||||||
|
/// [WidgetsBinding] for an explanation of rendering and non-rendering zones):
|
||||||
|
///
|
||||||
|
/// Most branches of the [Element] tree are expected to eventually insert a
|
||||||
|
/// [RenderObject] into their [RenderObjectElement] ancestor to construct the
|
||||||
|
/// render tree. However, there is a notable exception: an [Element] may
|
||||||
|
/// expect that the occupant of a certain child slot creates a new independent
|
||||||
|
/// render tree and therefore is not allowed to insert a render object into
|
||||||
|
/// the existing render tree. Those elements must return false from this
|
||||||
|
/// method for the slot in question to signal to the child in that slot that
|
||||||
|
/// it must not call [RenderObjectElement.insertRenderObjectChild] on its
|
||||||
|
/// ancestor.
|
||||||
|
///
|
||||||
|
/// As an example, the element backing the [ViewAnchor] returns false from
|
||||||
|
/// this method for the [ViewAnchor.view] slot to enforce that it is occupied
|
||||||
|
/// by e.g. a [View] widget, which will ultimately bootstrap a separate
|
||||||
|
/// render tree for that view. Another example is the [ViewCollection] widget,
|
||||||
|
/// which returns false for all its slots for the same reason.
|
||||||
|
///
|
||||||
|
/// Overriding this method is not common, as elements behaving in the way
|
||||||
|
/// described above are rare.
|
||||||
|
bool debugExpectsRenderObjectForSlot(Object? slot) => true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RenderObject? findRenderObject() {
|
RenderObject? findRenderObject() {
|
||||||
assert(() {
|
assert(() {
|
||||||
@ -5266,6 +5328,9 @@ abstract class ComponentElement extends Element {
|
|||||||
@override
|
@override
|
||||||
bool get debugDoingBuild => _debugDoingBuild;
|
bool get debugDoingBuild => _debugDoingBuild;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Element? get renderObjectAttachingChild => _child;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void mount(Element? parent, Object? newSlot) {
|
void mount(Element? parent, Object? newSlot) {
|
||||||
super.mount(parent, newSlot);
|
super.mount(parent, newSlot);
|
||||||
@ -6073,6 +6138,9 @@ abstract class RenderObjectElement extends Element {
|
|||||||
}
|
}
|
||||||
RenderObject? _renderObject;
|
RenderObject? _renderObject;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Element? get renderObjectAttachingChild => null;
|
||||||
|
|
||||||
bool _debugDoingBuild = false;
|
bool _debugDoingBuild = false;
|
||||||
@override
|
@override
|
||||||
bool get debugDoingBuild => _debugDoingBuild;
|
bool get debugDoingBuild => _debugDoingBuild;
|
||||||
@ -6082,8 +6150,25 @@ abstract class RenderObjectElement extends Element {
|
|||||||
RenderObjectElement? _findAncestorRenderObjectElement() {
|
RenderObjectElement? _findAncestorRenderObjectElement() {
|
||||||
Element? ancestor = _parent;
|
Element? ancestor = _parent;
|
||||||
while (ancestor != null && ancestor is! RenderObjectElement) {
|
while (ancestor != null && ancestor is! RenderObjectElement) {
|
||||||
ancestor = ancestor._parent;
|
// In debug mode we check whether the ancestor accepts RenderObjects to
|
||||||
|
// produce a better error message in attachRenderObject. In release mode,
|
||||||
|
// we assume only correct trees are built (i.e.
|
||||||
|
// debugExpectsRenderObjectForSlot always returns true) and don't check
|
||||||
|
// explicitly.
|
||||||
|
assert(() {
|
||||||
|
if (!ancestor!.debugExpectsRenderObjectForSlot(slot)) {
|
||||||
|
ancestor = null;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
ancestor = ancestor?._parent;
|
||||||
|
}
|
||||||
|
assert(() {
|
||||||
|
if (ancestor?.debugExpectsRenderObjectForSlot(slot) == false) {
|
||||||
|
ancestor = null;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
return ancestor as RenderObjectElement?;
|
return ancestor as RenderObjectElement?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6151,7 +6236,7 @@ abstract class RenderObjectElement extends Element {
|
|||||||
_debugUpdateRenderObjectOwner();
|
_debugUpdateRenderObjectOwner();
|
||||||
return true;
|
return true;
|
||||||
}());
|
}());
|
||||||
assert(_slot == newSlot);
|
assert(slot == newSlot);
|
||||||
attachRenderObject(newSlot);
|
attachRenderObject(newSlot);
|
||||||
super.performRebuild(); // clears the "dirty" flag
|
super.performRebuild(); // clears the "dirty" flag
|
||||||
}
|
}
|
||||||
@ -6252,12 +6337,13 @@ abstract class RenderObjectElement extends Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void _updateSlot(Object? newSlot) {
|
void updateSlot(Object? newSlot) {
|
||||||
final Object? oldSlot = slot;
|
final Object? oldSlot = slot;
|
||||||
assert(oldSlot != newSlot);
|
assert(oldSlot != newSlot);
|
||||||
super._updateSlot(newSlot);
|
super.updateSlot(newSlot);
|
||||||
assert(slot == newSlot);
|
assert(slot == newSlot);
|
||||||
_ancestorRenderObjectElement!.moveRenderObjectChild(renderObject, oldSlot, slot);
|
assert(_ancestorRenderObjectElement == _findAncestorRenderObjectElement());
|
||||||
|
_ancestorRenderObjectElement?.moveRenderObjectChild(renderObject, oldSlot, slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -6265,6 +6351,25 @@ abstract class RenderObjectElement extends Element {
|
|||||||
assert(_ancestorRenderObjectElement == null);
|
assert(_ancestorRenderObjectElement == null);
|
||||||
_slot = newSlot;
|
_slot = newSlot;
|
||||||
_ancestorRenderObjectElement = _findAncestorRenderObjectElement();
|
_ancestorRenderObjectElement = _findAncestorRenderObjectElement();
|
||||||
|
assert(() {
|
||||||
|
if (_ancestorRenderObjectElement == null) {
|
||||||
|
FlutterError.reportError(FlutterErrorDetails(exception: FlutterError.fromParts(
|
||||||
|
<DiagnosticsNode>[
|
||||||
|
ErrorSummary(
|
||||||
|
'The render object for ${toStringShort()} cannot find ancestor render object to attach to.',
|
||||||
|
),
|
||||||
|
ErrorDescription(
|
||||||
|
'The ownership chain for the RenderObject in question was:\n ${debugGetCreatorChain(10)}',
|
||||||
|
),
|
||||||
|
ErrorHint(
|
||||||
|
'Try wrapping your widget in a View widget or any other widget that is backed by '
|
||||||
|
'a $RenderTreeRootElement to serve as the root of the render tree.',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
_ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot);
|
_ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot);
|
||||||
final ParentDataElement<ParentData>? parentDataElement = _findAncestorParentDataElement();
|
final ParentDataElement<ParentData>? parentDataElement = _findAncestorParentDataElement();
|
||||||
if (parentDataElement != null) {
|
if (parentDataElement != null) {
|
||||||
@ -6597,6 +6702,67 @@ class MultiChildRenderObjectElement extends RenderObjectElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A [RenderObjectElement] used to manage the root of a render tree.
|
||||||
|
///
|
||||||
|
/// Unlike any other render object element this element does not attempt to
|
||||||
|
/// attach its [renderObject] to the closest ancestor [RenderObjectElement].
|
||||||
|
/// Instead, subclasses must override [attachRenderObject] and
|
||||||
|
/// [detachRenderObject] to attach/detach the [renderObject] to whatever
|
||||||
|
/// instance manages the render tree (e.g. by assigning it to
|
||||||
|
/// [PipelineOwner.rootNode]).
|
||||||
|
abstract class RenderTreeRootElement extends RenderObjectElement {
|
||||||
|
/// Creates an element that uses the given widget as its configuration.
|
||||||
|
RenderTreeRootElement(super.widget);
|
||||||
|
|
||||||
|
@override
|
||||||
|
@mustCallSuper
|
||||||
|
void attachRenderObject(Object? newSlot) {
|
||||||
|
_slot = newSlot;
|
||||||
|
assert(_debugCheckMustNotAttachRenderObjectToAncestor());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@mustCallSuper
|
||||||
|
void detachRenderObject() {
|
||||||
|
_slot = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateSlot(Object? newSlot) {
|
||||||
|
super.updateSlot(newSlot);
|
||||||
|
assert(_debugCheckMustNotAttachRenderObjectToAncestor());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _debugCheckMustNotAttachRenderObjectToAncestor() {
|
||||||
|
if (!kDebugMode) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (_findAncestorRenderObjectElement() != null) {
|
||||||
|
throw FlutterError.fromParts(
|
||||||
|
<DiagnosticsNode>[
|
||||||
|
ErrorSummary(
|
||||||
|
'The RenderObject for ${toStringShort()} cannot maintain an independent render tree at its current location.',
|
||||||
|
),
|
||||||
|
ErrorDescription(
|
||||||
|
'The ownership chain for the RenderObject in question was:\n ${debugGetCreatorChain(10)}',
|
||||||
|
),
|
||||||
|
ErrorDescription(
|
||||||
|
'This RenderObject is the root of an independent render tree and it cannot '
|
||||||
|
'attach itself to an ancestor in an existing tree. The ancestor RenderObject, '
|
||||||
|
'however, expects that a child will be attached.',
|
||||||
|
),
|
||||||
|
ErrorHint(
|
||||||
|
'Try moving the subtree that contains the ${toStringShort()} widget into the '
|
||||||
|
'view property of a ViewAnchor widget or to the root of the widget tree, where '
|
||||||
|
'it is not expected to attach its RenderObject to a parent.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A wrapper class for the [Element] that is the creator of a [RenderObject].
|
/// A wrapper class for the [Element] that is the creator of a [RenderObject].
|
||||||
///
|
///
|
||||||
/// Setting a [DebugCreator] as [RenderObject.debugCreator] will lead to better
|
/// Setting a [DebugCreator] as [RenderObject.debugCreator] will lead to better
|
||||||
|
@ -47,25 +47,31 @@ class SemanticsDebugger extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SemanticsDebuggerState extends State<SemanticsDebugger> with WidgetsBindingObserver {
|
class _SemanticsDebuggerState extends State<SemanticsDebugger> with WidgetsBindingObserver {
|
||||||
late _SemanticsClient _client;
|
_SemanticsClient? _client;
|
||||||
|
PipelineOwner? _pipelineOwner;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// TODO(abarth): We shouldn't reach out to the WidgetsBinding.instance
|
|
||||||
// static here because we might not be in a tree that's attached to that
|
|
||||||
// binding. Instead, we should find a way to get to the PipelineOwner from
|
|
||||||
// the BuildContext.
|
|
||||||
_client = _SemanticsClient(WidgetsBinding.instance.pipelineOwner)
|
|
||||||
..addListener(_update);
|
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
final PipelineOwner newOwner = View.pipelineOwnerOf(context);
|
||||||
|
if (newOwner != _pipelineOwner) {
|
||||||
|
_client?.dispose();
|
||||||
|
_client = _SemanticsClient(newOwner)
|
||||||
|
..addListener(_update);
|
||||||
|
_pipelineOwner = newOwner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_client
|
_client?.dispose();
|
||||||
..removeListener(_update)
|
|
||||||
..dispose();
|
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@ -145,19 +151,15 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> with WidgetsBindi
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _performAction(Offset position, SemanticsAction action) {
|
void _performAction(Offset position, SemanticsAction action) {
|
||||||
_pipelineOwner.semanticsOwner?.performActionAt(position, action);
|
_pipelineOwner?.semanticsOwner?.performActionAt(position, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(abarth): This shouldn't be a static. We should get the pipeline owner
|
|
||||||
// from [context] somehow.
|
|
||||||
PipelineOwner get _pipelineOwner => WidgetsBinding.instance.pipelineOwner;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return CustomPaint(
|
return CustomPaint(
|
||||||
foregroundPainter: _SemanticsDebuggerPainter(
|
foregroundPainter: _SemanticsDebuggerPainter(
|
||||||
_pipelineOwner,
|
_pipelineOwner!,
|
||||||
_client.generation,
|
_client!.generation,
|
||||||
_lastPointerDownLocation, // in physical pixels
|
_lastPointerDownLocation, // in physical pixels
|
||||||
View.of(context).devicePixelRatio,
|
View.of(context).devicePixelRatio,
|
||||||
widget.labelStyle,
|
widget.labelStyle,
|
||||||
|
@ -2,48 +2,89 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:ui' show FlutterView;
|
import 'dart:collection';
|
||||||
|
import 'dart:ui' show FlutterView, SemanticsUpdate;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
import 'framework.dart';
|
import 'framework.dart';
|
||||||
import 'lookup_boundary.dart';
|
import 'lookup_boundary.dart';
|
||||||
import 'media_query.dart';
|
import 'media_query.dart';
|
||||||
|
|
||||||
/// Injects a [FlutterView] into the tree and makes it available to descendants
|
/// Bootstraps a render tree that is rendered into the provided [FlutterView].
|
||||||
/// within the same [LookupBoundary] via [View.of] and [View.maybeOf].
|
///
|
||||||
|
/// The content rendered into that view is determined by the provided [child].
|
||||||
|
/// Descendants within the same [LookupBoundary] can look up the view they are
|
||||||
|
/// rendered into via [View.of] and [View.maybeOf].
|
||||||
///
|
///
|
||||||
/// The provided [child] is wrapped in a [MediaQuery] constructed from the given
|
/// The provided [child] is wrapped in a [MediaQuery] constructed from the given
|
||||||
/// [view].
|
/// [view].
|
||||||
///
|
///
|
||||||
/// In a future version of Flutter, the functionality of this widget will be
|
|
||||||
/// extended to actually bootstrap the render tree that is going to be rendered
|
|
||||||
/// into the provided [view]. This will enable rendering content into multiple
|
|
||||||
/// [FlutterView]s from a single widget tree.
|
|
||||||
///
|
|
||||||
/// Each [FlutterView] can be associated with at most one [View] widget in the
|
/// Each [FlutterView] can be associated with at most one [View] widget in the
|
||||||
/// widget tree. Two or more [View] widgets configured with the same
|
/// widget tree. Two or more [View] widgets configured with the same
|
||||||
/// [FlutterView] must never exist within the same widget tree at the same time.
|
/// [FlutterView] must never exist within the same widget tree at the same time.
|
||||||
/// Internally, this limitation is enforced by a [GlobalObjectKey] that derives
|
/// This limitation is enforced by a [GlobalObjectKey] that derives its identity
|
||||||
/// its identity from the [view] provided to this widget.
|
/// from the [view] provided to this widget.
|
||||||
|
///
|
||||||
|
/// Since the [View] widget bootstraps its own independent render tree, neither
|
||||||
|
/// it nor any of its descendants will insert a [RenderObject] into an existing
|
||||||
|
/// render tree. Therefore, the [View] widget can only be used in those parts of
|
||||||
|
/// the widget tree where it is not required to participate in the construction
|
||||||
|
/// of the surrounding render tree. In other words, the widget may only be used
|
||||||
|
/// in a non-rendering zone of the widget tree (see [WidgetsBinding] for a
|
||||||
|
/// definition of rendering and non-rendering zones).
|
||||||
|
///
|
||||||
|
/// In practical terms, the widget is typically used at the root of the widget
|
||||||
|
/// tree outside of any other [View] widget, as a child of a [ViewCollection]
|
||||||
|
/// widget, or in the [ViewAnchor.view] slot of a [ViewAnchor] widget. It is not
|
||||||
|
/// required to be a direct child, though, since other non-[RenderObjectWidget]s
|
||||||
|
/// (e.g. [InheritedWidget]s, [Builder]s, or [StatefulWidget]s/[StatelessWidget]
|
||||||
|
/// that only produce non-[RenderObjectWidget]s) are allowed to be present
|
||||||
|
/// between those widgets and the [View] widget.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [Element.debugExpectsRenderObjectForSlot], which defines whether a [View]
|
||||||
|
/// widget is allowed in a given child slot.
|
||||||
class View extends StatelessWidget {
|
class View extends StatelessWidget {
|
||||||
/// Injects the provided [view] into the widget tree.
|
/// Create a [View] widget to bootstrap a render tree that is rendered into
|
||||||
View({required this.view, required this.child}) : super(key: GlobalObjectKey(view));
|
/// the provided [FlutterView].
|
||||||
|
///
|
||||||
|
/// The content rendered into that [view] is determined by the given [child]
|
||||||
|
/// widget.
|
||||||
|
View({
|
||||||
|
super.key,
|
||||||
|
required this.view,
|
||||||
|
@Deprecated(
|
||||||
|
'Do not use. '
|
||||||
|
'This parameter only exists to implement the deprecated RendererBinding.pipelineOwner property until it is removed. '
|
||||||
|
'This feature was deprecated after v3.10.0-12.0.pre.'
|
||||||
|
)
|
||||||
|
PipelineOwner? deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner,
|
||||||
|
@Deprecated(
|
||||||
|
'Do not use. '
|
||||||
|
'This parameter only exists to implement the deprecated RendererBinding.renderView property until it is removed. '
|
||||||
|
'This feature was deprecated after v3.10.0-12.0.pre.'
|
||||||
|
)
|
||||||
|
RenderView? deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView,
|
||||||
|
required this.child,
|
||||||
|
}) : _deprecatedPipelineOwner = deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner,
|
||||||
|
_deprecatedRenderView = deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView,
|
||||||
|
assert((deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner == null) == (deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView == null)),
|
||||||
|
assert(deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView == null || deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView.flutterView == view);
|
||||||
|
|
||||||
/// The [FlutterView] to be injected into the tree.
|
/// The [FlutterView] into which [child] is drawn.
|
||||||
final FlutterView view;
|
final FlutterView view;
|
||||||
|
|
||||||
|
/// The widget below this widget in the tree, which will be drawn into the
|
||||||
|
/// [view].
|
||||||
|
///
|
||||||
/// {@macro flutter.widgets.ProxyWidget.child}
|
/// {@macro flutter.widgets.ProxyWidget.child}
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
@override
|
final PipelineOwner? _deprecatedPipelineOwner;
|
||||||
Widget build(BuildContext context) {
|
final RenderView? _deprecatedRenderView;
|
||||||
return _ViewScope(
|
|
||||||
view: view,
|
|
||||||
child: MediaQuery.fromView(
|
|
||||||
view: view,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the [FlutterView] that the provided `context` will render into.
|
/// Returns the [FlutterView] that the provided `context` will render into.
|
||||||
///
|
///
|
||||||
@ -106,13 +147,588 @@ class View extends StatelessWidget {
|
|||||||
}());
|
}());
|
||||||
return result!;
|
return result!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the [PipelineOwner] parent to which a child [View] should attach
|
||||||
|
/// its [PipelineOwner] to.
|
||||||
|
///
|
||||||
|
/// If `context` has a [View] ancestor, it returns the [PipelineOwner]
|
||||||
|
/// responsible for managing the render tree of that view. If there is no
|
||||||
|
/// [View] ancestor, [RendererBinding.rootPipelineOwner] is returned instead.
|
||||||
|
static PipelineOwner pipelineOwnerOf(BuildContext context) {
|
||||||
|
return context.dependOnInheritedWidgetOfExactType<_PipelineOwnerScope>()?.pipelineOwner
|
||||||
|
?? RendererBinding.instance.rootPipelineOwner;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return _RawView(
|
||||||
|
view: view,
|
||||||
|
deprecatedPipelineOwner: _deprecatedPipelineOwner,
|
||||||
|
deprecatedRenderView: _deprecatedRenderView,
|
||||||
|
builder: (BuildContext context, PipelineOwner owner) {
|
||||||
|
return _ViewScope(
|
||||||
|
view: view,
|
||||||
|
child: _PipelineOwnerScope(
|
||||||
|
pipelineOwner: owner,
|
||||||
|
child: MediaQuery.fromView(
|
||||||
|
view: view,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A builder for the content [Widget] of a [_RawView].
|
||||||
|
///
|
||||||
|
/// The widget returned by the builder defines the content that is drawn into
|
||||||
|
/// the [FlutterView] configured on the [_RawView].
|
||||||
|
///
|
||||||
|
/// The builder is given the [PipelineOwner] that the [_RawView] uses to manage
|
||||||
|
/// its render tree. Typical builder implementations make that pipeline owner
|
||||||
|
/// available as an attachment point for potential child views.
|
||||||
|
///
|
||||||
|
/// Used by [_RawView.builder].
|
||||||
|
typedef _RawViewContentBuilder = Widget Function(BuildContext context, PipelineOwner owner);
|
||||||
|
|
||||||
|
/// The workhorse behind the [View] widget that actually bootstraps a render
|
||||||
|
/// tree.
|
||||||
|
///
|
||||||
|
/// It instantiates the [RenderView] as the root of that render tree and adds it
|
||||||
|
/// to the [RendererBinding] via [RendererBinding.addRenderView]. It also owns
|
||||||
|
/// the [PipelineOwner] that manages this render tree and adds it as a child to
|
||||||
|
/// the surrounding parent [PipelineOwner] obtained with [View.pipelineOwnerOf].
|
||||||
|
/// This ensures that the render tree bootstrapped by this widget participates
|
||||||
|
/// properly in frame production and hit testing.
|
||||||
|
class _RawView extends RenderObjectWidget {
|
||||||
|
/// Create a [RawView] widget to bootstrap a render tree that is rendered into
|
||||||
|
/// the provided [FlutterView].
|
||||||
|
///
|
||||||
|
/// The content rendered into that [view] is determined by the [Widget]
|
||||||
|
/// returned by [builder].
|
||||||
|
_RawView({
|
||||||
|
required this.view,
|
||||||
|
required PipelineOwner? deprecatedPipelineOwner,
|
||||||
|
required RenderView? deprecatedRenderView,
|
||||||
|
required this.builder,
|
||||||
|
}) : _deprecatedPipelineOwner = deprecatedPipelineOwner,
|
||||||
|
_deprecatedRenderView = deprecatedRenderView,
|
||||||
|
assert(deprecatedRenderView == null || deprecatedRenderView.flutterView == view),
|
||||||
|
// TODO(goderbauer): Replace this with GlobalObjectKey(view) when the deprecated properties are removed.
|
||||||
|
super(key: _DeprecatedRawViewKey(view, deprecatedPipelineOwner, deprecatedRenderView));
|
||||||
|
|
||||||
|
/// The [FlutterView] into which the [Widget] returned by [builder] is drawn.
|
||||||
|
final FlutterView view;
|
||||||
|
|
||||||
|
/// Determines the content [Widget] that is drawn into the [view].
|
||||||
|
///
|
||||||
|
/// The [builder] is given the [PipelineOwner] responsible for the render tree
|
||||||
|
/// bootstrapped by this widget and typically makes it available as an
|
||||||
|
/// attachment point for potential child views.
|
||||||
|
final _RawViewContentBuilder builder;
|
||||||
|
|
||||||
|
final PipelineOwner? _deprecatedPipelineOwner;
|
||||||
|
final RenderView? _deprecatedRenderView;
|
||||||
|
|
||||||
|
@override
|
||||||
|
RenderObjectElement createElement() => _RawViewElement(this);
|
||||||
|
|
||||||
|
@override
|
||||||
|
RenderObject createRenderObject(BuildContext context) {
|
||||||
|
return _deprecatedRenderView ?? RenderView(
|
||||||
|
view: view,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No need to implement updateRenderObject: RawView uses the view as a
|
||||||
|
// GlobalKey, so we never need to update the RenderObject with a new view.
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RawViewElement extends RenderTreeRootElement {
|
||||||
|
_RawViewElement(super.widget);
|
||||||
|
|
||||||
|
late final PipelineOwner _pipelineOwner = PipelineOwner(
|
||||||
|
onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,
|
||||||
|
onSemanticsUpdate: _handleSemanticsUpdate,
|
||||||
|
onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,
|
||||||
|
);
|
||||||
|
|
||||||
|
PipelineOwner get _effectivePipelineOwner => (widget as _RawView)._deprecatedPipelineOwner ?? _pipelineOwner;
|
||||||
|
|
||||||
|
void _handleSemanticsOwnerCreated() {
|
||||||
|
(_effectivePipelineOwner.rootNode as RenderView?)?.scheduleInitialSemantics();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSemanticsOwnerDisposed() {
|
||||||
|
(_effectivePipelineOwner.rootNode as RenderView?)?.clearSemantics();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSemanticsUpdate(SemanticsUpdate update) {
|
||||||
|
(widget as _RawView).view.updateSemantics(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
RenderView get renderObject => super.renderObject as RenderView;
|
||||||
|
|
||||||
|
Element? _child;
|
||||||
|
|
||||||
|
void _updateChild() {
|
||||||
|
try {
|
||||||
|
final Widget child = (widget as _RawView).builder(this, _effectivePipelineOwner);
|
||||||
|
_child = updateChild(_child, child, null);
|
||||||
|
} catch (e, stack) {
|
||||||
|
final FlutterErrorDetails details = FlutterErrorDetails(
|
||||||
|
exception: e,
|
||||||
|
stack: stack,
|
||||||
|
library: 'widgets library',
|
||||||
|
context: ErrorDescription('building $this'),
|
||||||
|
informationCollector: !kDebugMode ? null : () => <DiagnosticsNode>[
|
||||||
|
DiagnosticsDebugCreator(DebugCreator(this)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
FlutterError.reportError(details);
|
||||||
|
final Widget error = ErrorWidget.builder(details);
|
||||||
|
_child = updateChild(null, error, slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void mount(Element? parent, Object? newSlot) {
|
||||||
|
super.mount(parent, newSlot);
|
||||||
|
assert(_effectivePipelineOwner.rootNode == null);
|
||||||
|
_effectivePipelineOwner.rootNode = renderObject;
|
||||||
|
_attachView();
|
||||||
|
_updateChild();
|
||||||
|
renderObject.prepareInitialFrame();
|
||||||
|
if (_effectivePipelineOwner.semanticsOwner != null) {
|
||||||
|
renderObject.scheduleInitialSemantics();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PipelineOwner? _parentPipelineOwner; // Is null if view is currently not attached.
|
||||||
|
|
||||||
|
void _attachView([PipelineOwner? parentPipelineOwner]) {
|
||||||
|
assert(_parentPipelineOwner == null);
|
||||||
|
parentPipelineOwner ??= View.pipelineOwnerOf(this);
|
||||||
|
parentPipelineOwner.adoptChild(_effectivePipelineOwner);
|
||||||
|
RendererBinding.instance.addRenderView(renderObject);
|
||||||
|
_parentPipelineOwner = parentPipelineOwner;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _detachView() {
|
||||||
|
final PipelineOwner? parentPipelineOwner = _parentPipelineOwner;
|
||||||
|
if (parentPipelineOwner != null) {
|
||||||
|
RendererBinding.instance.removeRenderView(renderObject);
|
||||||
|
parentPipelineOwner.dropChild(_effectivePipelineOwner);
|
||||||
|
_parentPipelineOwner = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
if (_parentPipelineOwner == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final PipelineOwner newParentPipelineOwner = View.pipelineOwnerOf(this);
|
||||||
|
if (newParentPipelineOwner != _parentPipelineOwner) {
|
||||||
|
_detachView();
|
||||||
|
_attachView(newParentPipelineOwner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void performRebuild() {
|
||||||
|
super.performRebuild();
|
||||||
|
_updateChild();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void activate() {
|
||||||
|
super.activate();
|
||||||
|
assert(_effectivePipelineOwner.rootNode == null);
|
||||||
|
_effectivePipelineOwner.rootNode = renderObject;
|
||||||
|
_attachView();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void deactivate() {
|
||||||
|
_detachView();
|
||||||
|
assert(_effectivePipelineOwner.rootNode == renderObject);
|
||||||
|
_effectivePipelineOwner.rootNode = null; // To satisfy the assert in the super class.
|
||||||
|
super.deactivate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void update(_RawView newWidget) {
|
||||||
|
super.update(newWidget);
|
||||||
|
_updateChild();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void visitChildren(ElementVisitor visitor) {
|
||||||
|
if (_child != null) {
|
||||||
|
visitor(_child!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void forgetChild(Element child) {
|
||||||
|
assert(child == _child);
|
||||||
|
_child = null;
|
||||||
|
super.forgetChild(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void insertRenderObjectChild(RenderBox child, Object? slot) {
|
||||||
|
assert(slot == null);
|
||||||
|
assert(renderObject.debugValidateChild(child));
|
||||||
|
renderObject.child = child;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void moveRenderObjectChild(RenderObject child, Object? oldSlot, Object? newSlot) {
|
||||||
|
assert(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void removeRenderObjectChild(RenderObject child, Object? slot) {
|
||||||
|
assert(slot == null);
|
||||||
|
assert(renderObject.child == child);
|
||||||
|
renderObject.child = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void unmount() {
|
||||||
|
if (_effectivePipelineOwner != (widget as _RawView)._deprecatedPipelineOwner) {
|
||||||
|
_effectivePipelineOwner.dispose();
|
||||||
|
}
|
||||||
|
super.unmount();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ViewScope extends InheritedWidget {
|
class _ViewScope extends InheritedWidget {
|
||||||
const _ViewScope({required this.view, required super.child});
|
const _ViewScope({required this.view, required super.child});
|
||||||
|
|
||||||
final FlutterView view;
|
final FlutterView? view;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool updateShouldNotify(_ViewScope oldWidget) => view != oldWidget.view;
|
bool updateShouldNotify(_ViewScope oldWidget) => view != oldWidget.view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _PipelineOwnerScope extends InheritedWidget {
|
||||||
|
const _PipelineOwnerScope({
|
||||||
|
required this.pipelineOwner,
|
||||||
|
required super.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
final PipelineOwner pipelineOwner;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(_PipelineOwnerScope oldWidget) => pipelineOwner != oldWidget.pipelineOwner;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MultiChildComponentWidget extends Widget {
|
||||||
|
const _MultiChildComponentWidget({
|
||||||
|
super.key,
|
||||||
|
List<Widget> views = const <Widget>[],
|
||||||
|
Widget? child,
|
||||||
|
}) : _views = views, _child = child;
|
||||||
|
|
||||||
|
// It is up to the subclasses to make the relevant properties public.
|
||||||
|
final List<Widget> _views;
|
||||||
|
final Widget? _child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Element createElement() => _MultiChildComponentElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A collection of sibling [View]s.
|
||||||
|
///
|
||||||
|
/// This widget can only be used in places were a [View] widget is allowed, i.e.
|
||||||
|
/// in a non-rendering zone of the widget tree. In practical terms, it can be
|
||||||
|
/// used at the root of the widget tree outside of any [View] widget, as a child
|
||||||
|
/// to a another [ViewCollection], or in the [ViewAnchor.view] slot of a
|
||||||
|
/// [ViewAnchor] widget. It is not required to be a direct child of those
|
||||||
|
/// widgets; other non-[RenderObjectWidget]s may appear in between the two (such
|
||||||
|
/// as an [InheritedWidget]).
|
||||||
|
///
|
||||||
|
/// Similarly, the [views] children of this widget must be [View]s, but they
|
||||||
|
/// may be wrapped in additional non-[RenderObjectWidget]s (e.g.
|
||||||
|
/// [InheritedWidget]s).
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [WidgetsBinding] for an explanation of rendering and non-rendering zones.
|
||||||
|
class ViewCollection extends _MultiChildComponentWidget {
|
||||||
|
/// Creates a [ViewCollection] widget.
|
||||||
|
///
|
||||||
|
/// The provided list of [views] must contain at least one widget.
|
||||||
|
const ViewCollection({super.key, required super.views}) : assert(views.length > 0);
|
||||||
|
|
||||||
|
/// The [View] descendants of this widget.
|
||||||
|
///
|
||||||
|
/// The [View]s may be wrapped in other non-[RenderObjectWidget]s (e.g.
|
||||||
|
/// [InheritedWidget]s). However, no [RenderObjectWidget] is allowed to appear
|
||||||
|
/// between the [ViewCollection] and the next [View] widget.
|
||||||
|
List<Widget> get views => _views;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decorates a [child] widget with a side [View].
|
||||||
|
///
|
||||||
|
/// This widget must have a [View] ancestor, into which the [child] widget
|
||||||
|
/// is rendered.
|
||||||
|
///
|
||||||
|
/// Typically, a [View] or [ViewCollection] widget is used in the [view] slot to
|
||||||
|
/// define the content of the side view(s). Those widgets may be wrapped in
|
||||||
|
/// other non-[RenderObjectWidget]s (e.g. [InheritedWidget]s). However, no
|
||||||
|
/// [RenderObjectWidget] is allowed to appear between the [ViewAnchor] and the
|
||||||
|
/// next [View] widget in the [view] slot. The widgets in the [view] slot have
|
||||||
|
/// access to all [InheritedWidget]s above the [ViewAnchor] in the tree.
|
||||||
|
///
|
||||||
|
/// In technical terms, the [ViewAnchor] can only be used in a rendering zone of
|
||||||
|
/// the widget tree and the [view] slot marks the start of a new non-rendering
|
||||||
|
/// zone (see [WidgetsBinding] for a definition of these zones). Typically,
|
||||||
|
/// it is occupied by a [View] widget, which will start a new rendering zone.
|
||||||
|
///
|
||||||
|
/// {@template flutter.widgets.ViewAnchor}
|
||||||
|
/// An example use case for this widget is a tooltip for a button. The tooltip
|
||||||
|
/// should be able to extend beyond the bounds of the main view. For this, the
|
||||||
|
/// tooltip can be implemented as a separate [View], which is anchored to the
|
||||||
|
/// button in the main view by wrapping that button with a [ViewAnchor]. In this
|
||||||
|
/// example, the [view] slot is configured with the tooltip [View] and the
|
||||||
|
/// [child] is the button widget rendered into the surrounding view.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class ViewAnchor extends StatelessWidget {
|
||||||
|
/// Creates a [ViewAnchor] widget.
|
||||||
|
const ViewAnchor({
|
||||||
|
super.key,
|
||||||
|
this.view,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The widget that defines the view anchored to this widget.
|
||||||
|
///
|
||||||
|
/// Typically, a [View] or [ViewCollection] widget is used, which may be
|
||||||
|
/// wrapped in other non-[RenderObjectWidget]s (e.g. [InheritedWidget]s).
|
||||||
|
///
|
||||||
|
/// {@macro flutter.widgets.ViewAnchor}
|
||||||
|
final Widget? view;
|
||||||
|
|
||||||
|
/// The widget below this widget in the tree.
|
||||||
|
///
|
||||||
|
/// It is rendered into the surrounding view, not in the view defined by
|
||||||
|
/// [view].
|
||||||
|
///
|
||||||
|
/// {@macro flutter.widgets.ViewAnchor}
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return _MultiChildComponentWidget(
|
||||||
|
views: <Widget>[
|
||||||
|
if (view != null)
|
||||||
|
_ViewScope(
|
||||||
|
view: null,
|
||||||
|
child: view!,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MultiChildComponentElement extends Element {
|
||||||
|
_MultiChildComponentElement(super.widget);
|
||||||
|
|
||||||
|
List<Element> _viewElements = <Element>[];
|
||||||
|
final Set<Element> _forgottenViewElements = HashSet<Element>();
|
||||||
|
Element? _childElement;
|
||||||
|
|
||||||
|
bool _debugAssertChildren() {
|
||||||
|
final _MultiChildComponentWidget typedWidget = widget as _MultiChildComponentWidget;
|
||||||
|
// Each view widget must have a corresponding element.
|
||||||
|
assert(_viewElements.length == typedWidget._views.length);
|
||||||
|
// Iff there is a child widget, it must have a corresponding element.
|
||||||
|
assert((_childElement == null) == (typedWidget._child == null));
|
||||||
|
// The child element is not also a view element.
|
||||||
|
assert(!_viewElements.contains(_childElement));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void attachRenderObject(Object? newSlot) {
|
||||||
|
super.attachRenderObject(newSlot);
|
||||||
|
assert(_debugCheckMustAttachRenderObject(newSlot));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void mount(Element? parent, Object? newSlot) {
|
||||||
|
super.mount(parent, newSlot);
|
||||||
|
assert(_debugCheckMustAttachRenderObject(newSlot));
|
||||||
|
assert(_viewElements.isEmpty);
|
||||||
|
assert(_childElement == null);
|
||||||
|
rebuild();
|
||||||
|
assert(_debugAssertChildren());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateSlot(Object? newSlot) {
|
||||||
|
super.updateSlot(newSlot);
|
||||||
|
assert(_debugCheckMustAttachRenderObject(newSlot));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _debugCheckMustAttachRenderObject(Object? slot) {
|
||||||
|
// Check only applies in the ViewCollection configuration.
|
||||||
|
if (!kDebugMode || (widget as _MultiChildComponentWidget)._child != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool hasAncestorRenderObjectElement = false;
|
||||||
|
bool ancestorWantsRenderObject = true;
|
||||||
|
visitAncestorElements((Element ancestor) {
|
||||||
|
if (!ancestor.debugExpectsRenderObjectForSlot(slot)) {
|
||||||
|
ancestorWantsRenderObject = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ancestor is RenderObjectElement) {
|
||||||
|
hasAncestorRenderObjectElement = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (hasAncestorRenderObjectElement && ancestorWantsRenderObject) {
|
||||||
|
FlutterError.reportError(
|
||||||
|
FlutterErrorDetails(exception: FlutterError.fromParts(
|
||||||
|
<DiagnosticsNode>[
|
||||||
|
ErrorSummary(
|
||||||
|
'The Element for ${toStringShort()} cannot be inserted into slot "$slot" of its ancestor. ',
|
||||||
|
),
|
||||||
|
ErrorDescription(
|
||||||
|
'The ownership chain for the Element in question was:\n ${debugGetCreatorChain(10)}',
|
||||||
|
),
|
||||||
|
ErrorDescription(
|
||||||
|
'This Element allows the creation of multiple independent render trees, which cannot '
|
||||||
|
'be attached to an ancestor in an existing render tree. However, an ancestor RenderObject '
|
||||||
|
'is expecting that a child will be attached.'
|
||||||
|
),
|
||||||
|
ErrorHint(
|
||||||
|
'Try moving the subtree that contains the ${toStringShort()} widget into the '
|
||||||
|
'view property of a ViewAnchor widget or to the root of the widget tree, where '
|
||||||
|
'it is not expected to attach its RenderObject to its ancestor.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void update(_MultiChildComponentWidget newWidget) {
|
||||||
|
// Cannot switch from ViewAnchor config to ViewCollection config.
|
||||||
|
assert((newWidget._child == null) == ((widget as _MultiChildComponentWidget)._child == null));
|
||||||
|
super.update(newWidget);
|
||||||
|
rebuild(force: true);
|
||||||
|
assert(_debugAssertChildren());
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Object _viewSlot = Object();
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool debugExpectsRenderObjectForSlot(Object? slot) => slot != _viewSlot;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void performRebuild() {
|
||||||
|
final _MultiChildComponentWidget typedWidget = widget as _MultiChildComponentWidget;
|
||||||
|
|
||||||
|
_childElement = updateChild(_childElement, typedWidget._child, slot);
|
||||||
|
|
||||||
|
final List<Widget> views = typedWidget._views;
|
||||||
|
_viewElements = updateChildren(
|
||||||
|
_viewElements,
|
||||||
|
views,
|
||||||
|
forgottenChildren: _forgottenViewElements,
|
||||||
|
slots: List<Object>.generate(views.length, (_) => _viewSlot),
|
||||||
|
);
|
||||||
|
_forgottenViewElements.clear();
|
||||||
|
|
||||||
|
super.performRebuild(); // clears the dirty flag
|
||||||
|
assert(_debugAssertChildren());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void forgetChild(Element child) {
|
||||||
|
if (child == _childElement) {
|
||||||
|
_childElement = null;
|
||||||
|
} else {
|
||||||
|
assert(_viewElements.contains(child));
|
||||||
|
assert(!_forgottenViewElements.contains(child));
|
||||||
|
_forgottenViewElements.add(child);
|
||||||
|
}
|
||||||
|
super.forgetChild(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void visitChildren(ElementVisitor visitor) {
|
||||||
|
if (_childElement != null) {
|
||||||
|
visitor(_childElement!);
|
||||||
|
}
|
||||||
|
for (final Element child in _viewElements) {
|
||||||
|
if (!_forgottenViewElements.contains(child)) {
|
||||||
|
visitor(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get debugDoingBuild => false; // This element does not have a concept of "building".
|
||||||
|
|
||||||
|
@override
|
||||||
|
Element? get renderObjectAttachingChild => _childElement;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<DiagnosticsNode> debugDescribeChildren() {
|
||||||
|
final List<DiagnosticsNode> children = <DiagnosticsNode>[];
|
||||||
|
if (_childElement != null) {
|
||||||
|
children.add(_childElement!.toDiagnosticsNode());
|
||||||
|
}
|
||||||
|
for (int i = 0; i < _viewElements.length; i++) {
|
||||||
|
children.add(_viewElements[i].toDiagnosticsNode(
|
||||||
|
name: 'view ${i + 1}',
|
||||||
|
style: DiagnosticsTreeStyle.offstage,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A special [GlobalKey] to support passing the deprecated
|
||||||
|
// [RendererBinding.renderView] and [RendererBinding.pipelineOwner] to the
|
||||||
|
// [_RawView]. Will be removed when those deprecated properties are removed.
|
||||||
|
@optionalTypeArgs
|
||||||
|
class _DeprecatedRawViewKey<T extends State<StatefulWidget>> extends GlobalKey<T> {
|
||||||
|
const _DeprecatedRawViewKey(this.view, this.owner, this.renderView) : super.constructor();
|
||||||
|
|
||||||
|
final FlutterView view;
|
||||||
|
final PipelineOwner? owner;
|
||||||
|
final RenderView? renderView;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other.runtimeType != runtimeType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return other is _DeprecatedRawViewKey<T>
|
||||||
|
&& identical(other.view, view)
|
||||||
|
&& identical(other.owner, owner)
|
||||||
|
&& identical(other.renderView, renderView);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(view, owner, renderView);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => '[_DeprecatedRawViewKey ${describeIdentity(view)}]';
|
||||||
|
}
|
||||||
|
@ -1103,8 +1103,7 @@ mixin WidgetInspectorService {
|
|||||||
renderObject.markNeedsPaint();
|
renderObject.markNeedsPaint();
|
||||||
renderObject.visitChildren(markTreeNeedsPaint);
|
renderObject.visitChildren(markTreeNeedsPaint);
|
||||||
}
|
}
|
||||||
final RenderObject root = RendererBinding.instance.renderView;
|
RendererBinding.instance.renderViews.forEach(markTreeNeedsPaint);
|
||||||
markTreeNeedsPaint(root);
|
|
||||||
} else {
|
} else {
|
||||||
debugOnProfilePaint = null;
|
debugOnProfilePaint = null;
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ export 'package:vector_math/vector_math_64.dart' show Matrix4;
|
|||||||
export 'foundation.dart' show UniqueKey;
|
export 'foundation.dart' show UniqueKey;
|
||||||
export 'rendering.dart' show TextSelectionHandleType;
|
export 'rendering.dart' show TextSelectionHandleType;
|
||||||
export 'src/widgets/actions.dart';
|
export 'src/widgets/actions.dart';
|
||||||
|
export 'src/widgets/adapter.dart';
|
||||||
export 'src/widgets/animated_cross_fade.dart';
|
export 'src/widgets/animated_cross_fade.dart';
|
||||||
export 'src/widgets/animated_scroll_view.dart';
|
export 'src/widgets/animated_scroll_view.dart';
|
||||||
export 'src/widgets/animated_size.dart';
|
export 'src/widgets/animated_size.dart';
|
||||||
|
@ -117,9 +117,17 @@ Future<Map<String, dynamic>> hasReassemble(Future<Map<String, dynamic>> pendingR
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
final List<String?> console = <String?>[];
|
final List<String?> console = <String?>[];
|
||||||
|
late PipelineOwner owner;
|
||||||
|
|
||||||
setUpAll(() async {
|
setUpAll(() async {
|
||||||
binding = TestServiceExtensionsBinding()..scheduleFrame();
|
binding = TestServiceExtensionsBinding();
|
||||||
|
final RenderView view = RenderView(view: binding.platformDispatcher.views.single);
|
||||||
|
owner = PipelineOwner(onSemanticsUpdate: (ui.SemanticsUpdate _) { })
|
||||||
|
..rootNode = view;
|
||||||
|
binding.rootPipelineOwner.adoptChild(owner);
|
||||||
|
binding.addRenderView(view);
|
||||||
|
view.prepareInitialFrame();
|
||||||
|
binding.scheduleFrame();
|
||||||
expect(binding.frameScheduled, isTrue);
|
expect(binding.frameScheduled, isTrue);
|
||||||
|
|
||||||
// We need to test this service extension here because the result is true
|
// We need to test this service extension here because the result is true
|
||||||
@ -176,6 +184,10 @@ void main() {
|
|||||||
|
|
||||||
expect(console, isEmpty);
|
expect(console, isEmpty);
|
||||||
debugPrint = debugPrintThrottled;
|
debugPrint = debugPrintThrottled;
|
||||||
|
binding.rootPipelineOwner.dropChild(owner);
|
||||||
|
owner
|
||||||
|
..rootNode = null
|
||||||
|
..dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
// The following list is alphabetical, one test per extension.
|
// The following list is alphabetical, one test per extension.
|
||||||
@ -268,11 +280,13 @@ void main() {
|
|||||||
await binding.doFrame();
|
await binding.doFrame();
|
||||||
final Map<String, dynamic> result = await binding.testExtension(RenderingServiceExtensions.debugDumpSemanticsTreeInTraversalOrder.name, <String, String>{});
|
final Map<String, dynamic> result = await binding.testExtension(RenderingServiceExtensions.debugDumpSemanticsTreeInTraversalOrder.name, <String, String>{});
|
||||||
|
|
||||||
expect(result, <String, String>{
|
expect(result, <String, Object>{
|
||||||
'data': 'Semantics not generated.\n'
|
'data': matches(
|
||||||
'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n'
|
r'Semantics not generated for RenderView#[0-9a-f]{5}\.\n'
|
||||||
'Usually, platforms only ask for semantics when assistive technologies (like screen readers) are running.\n'
|
r'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n'
|
||||||
'To generate semantics, try turning on an assistive technology (like VoiceOver or TalkBack) on your device.'
|
r'Usually, platforms only ask for semantics when assistive technologies \(like screen readers\) are running.\n'
|
||||||
|
r'To generate semantics, try turning on an assistive technology \(like VoiceOver or TalkBack\) on your device.'
|
||||||
|
)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -280,11 +294,13 @@ void main() {
|
|||||||
await binding.doFrame();
|
await binding.doFrame();
|
||||||
final Map<String, dynamic> result = await binding.testExtension(RenderingServiceExtensions.debugDumpSemanticsTreeInInverseHitTestOrder.name, <String, String>{});
|
final Map<String, dynamic> result = await binding.testExtension(RenderingServiceExtensions.debugDumpSemanticsTreeInInverseHitTestOrder.name, <String, String>{});
|
||||||
|
|
||||||
expect(result, <String, String>{
|
expect(result, <String, Object>{
|
||||||
'data': 'Semantics not generated.\n'
|
'data': matches(
|
||||||
'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n'
|
r'Semantics not generated for RenderView#[0-9a-f]{5}\.\n'
|
||||||
'Usually, platforms only ask for semantics when assistive technologies (like screen readers) are running.\n'
|
r'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n'
|
||||||
'To generate semantics, try turning on an assistive technology (like VoiceOver or TalkBack) on your device.'
|
r'Usually, platforms only ask for semantics when assistive technologies \(like screen readers\) are running.\n'
|
||||||
|
r'To generate semantics, try turning on an assistive technology \(like VoiceOver or TalkBack\) on your device.'
|
||||||
|
)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -13,20 +13,20 @@ void main() {
|
|||||||
|
|
||||||
tearDown(() {
|
tearDown(() {
|
||||||
final List<PipelineOwner> children = <PipelineOwner>[];
|
final List<PipelineOwner> children = <PipelineOwner>[];
|
||||||
RendererBinding.instance.pipelineOwner.visitChildren((PipelineOwner child) {
|
RendererBinding.instance.rootPipelineOwner.visitChildren((PipelineOwner child) {
|
||||||
children.add(child);
|
children.add(child);
|
||||||
});
|
});
|
||||||
children.forEach(RendererBinding.instance.pipelineOwner.dropChild);
|
children.forEach(RendererBinding.instance.rootPipelineOwner.dropChild);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("BindingPipelineManifold notifies binding if render object managed by binding's PipelineOwner tree needs visual update", () {
|
test("BindingPipelineManifold notifies binding if render object managed by binding's PipelineOwner tree needs visual update", () {
|
||||||
final PipelineOwner child = PipelineOwner();
|
final PipelineOwner child = PipelineOwner();
|
||||||
RendererBinding.instance.pipelineOwner.adoptChild(child);
|
RendererBinding.instance.rootPipelineOwner.adoptChild(child);
|
||||||
|
|
||||||
final RenderObject renderObject = TestRenderObject();
|
final RenderObject renderObject = TestRenderObject();
|
||||||
child.rootNode = renderObject;
|
child.rootNode = renderObject;
|
||||||
renderObject.scheduleInitialLayout();
|
renderObject.scheduleInitialLayout();
|
||||||
RendererBinding.instance.pipelineOwner.flushLayout();
|
RendererBinding.instance.rootPipelineOwner.flushLayout();
|
||||||
|
|
||||||
MyTestRenderingFlutterBinding.instance.ensureVisualUpdateCount = 0;
|
MyTestRenderingFlutterBinding.instance.ensureVisualUpdateCount = 0;
|
||||||
renderObject.markNeedsLayout();
|
renderObject.markNeedsLayout();
|
||||||
@ -37,20 +37,20 @@ void main() {
|
|||||||
final PipelineOwner child = PipelineOwner(
|
final PipelineOwner child = PipelineOwner(
|
||||||
onSemanticsUpdate: (_) { },
|
onSemanticsUpdate: (_) { },
|
||||||
);
|
);
|
||||||
RendererBinding.instance.pipelineOwner.adoptChild(child);
|
RendererBinding.instance.rootPipelineOwner.adoptChild(child);
|
||||||
|
|
||||||
expect(child.semanticsOwner, isNull);
|
expect(child.semanticsOwner, isNull);
|
||||||
expect(RendererBinding.instance.pipelineOwner.semanticsOwner, isNull);
|
expect(RendererBinding.instance.rootPipelineOwner.semanticsOwner, isNull);
|
||||||
|
|
||||||
final SemanticsHandle handle = SemanticsBinding.instance.ensureSemantics();
|
final SemanticsHandle handle = SemanticsBinding.instance.ensureSemantics();
|
||||||
|
|
||||||
expect(child.semanticsOwner, isNotNull);
|
expect(child.semanticsOwner, isNotNull);
|
||||||
expect(RendererBinding.instance.pipelineOwner.semanticsOwner, isNotNull);
|
expect(RendererBinding.instance.rootPipelineOwner.semanticsOwner, isNotNull);
|
||||||
|
|
||||||
handle.dispose();
|
handle.dispose();
|
||||||
|
|
||||||
expect(child.semanticsOwner, isNull);
|
expect(child.semanticsOwner, isNull);
|
||||||
expect(RendererBinding.instance.pipelineOwner.semanticsOwner, isNull);
|
expect(RendererBinding.instance.rootPipelineOwner.semanticsOwner, isNull);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,29 +10,48 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
void main() {
|
void main() {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
test('handleMetricsChanged does not scheduleForcedFrame unless there is a child to the renderView', () async {
|
test('handleMetricsChanged does not scheduleForcedFrame unless there a registered renderView with a child', () async {
|
||||||
expect(SchedulerBinding.instance.hasScheduledFrame, false);
|
expect(SchedulerBinding.instance.hasScheduledFrame, false);
|
||||||
RendererBinding.instance.handleMetricsChanged();
|
RendererBinding.instance.handleMetricsChanged();
|
||||||
expect(SchedulerBinding.instance.hasScheduledFrame, false);
|
expect(SchedulerBinding.instance.hasScheduledFrame, false);
|
||||||
|
|
||||||
|
RendererBinding.instance.addRenderView(RendererBinding.instance.renderView);
|
||||||
|
RendererBinding.instance.handleMetricsChanged();
|
||||||
|
expect(SchedulerBinding.instance.hasScheduledFrame, false);
|
||||||
|
|
||||||
RendererBinding.instance.renderView.child = RenderLimitedBox();
|
RendererBinding.instance.renderView.child = RenderLimitedBox();
|
||||||
RendererBinding.instance.handleMetricsChanged();
|
RendererBinding.instance.handleMetricsChanged();
|
||||||
expect(SchedulerBinding.instance.hasScheduledFrame, true);
|
expect(SchedulerBinding.instance.hasScheduledFrame, true);
|
||||||
|
|
||||||
|
RendererBinding.instance.removeRenderView(RendererBinding.instance.renderView);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('debugDumpSemantics prints explanation when semantics are unavailable', () {
|
test('debugDumpSemantics prints explanation when semantics are unavailable', () {
|
||||||
|
RendererBinding.instance.addRenderView(RendererBinding.instance.renderView);
|
||||||
final List<String?> log = <String?>[];
|
final List<String?> log = <String?>[];
|
||||||
debugPrint = (String? message, {int? wrapWidth}) {
|
debugPrint = (String? message, {int? wrapWidth}) {
|
||||||
log.add(message);
|
log.add(message);
|
||||||
};
|
};
|
||||||
debugDumpSemanticsTree();
|
debugDumpSemanticsTree();
|
||||||
expect(log, hasLength(1));
|
expect(log, hasLength(1));
|
||||||
expect(
|
expect(log.single, startsWith('Semantics not generated'));
|
||||||
log.single,
|
expect(log.single, endsWith(
|
||||||
'Semantics not generated.\n'
|
|
||||||
'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n'
|
'For performance reasons, the framework only generates semantics when asked to do so by the platform.\n'
|
||||||
'Usually, platforms only ask for semantics when assistive technologies (like screen readers) are running.\n'
|
'Usually, platforms only ask for semantics when assistive technologies (like screen readers) are running.\n'
|
||||||
'To generate semantics, try turning on an assistive technology (like VoiceOver or TalkBack) on your device.'
|
'To generate semantics, try turning on an assistive technology (like VoiceOver or TalkBack) on your device.'
|
||||||
|
));
|
||||||
|
RendererBinding.instance.removeRenderView(RendererBinding.instance.renderView);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('root pipeline owner cannot manage root node', () {
|
||||||
|
final RenderObject rootNode = RenderProxyBox();
|
||||||
|
expect(
|
||||||
|
() => RendererBinding.instance.rootPipelineOwner.rootNode = rootNode,
|
||||||
|
throwsA(isFlutterError.having(
|
||||||
|
(FlutterError e) => e.message,
|
||||||
|
'message',
|
||||||
|
contains('Cannot set a rootNode on the default root pipeline owner.'),
|
||||||
|
)),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -32,8 +32,21 @@ class TestMouseTrackerFlutterBinding extends BindingBase
|
|||||||
postFrameCallbacks = <void Function(Duration)>[];
|
postFrameCallbacks = <void Function(Duration)>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
late final RenderView _renderView = RenderView(
|
||||||
|
view: platformDispatcher.implicitView!,
|
||||||
|
);
|
||||||
|
|
||||||
|
late final PipelineOwner _pipelineOwner = PipelineOwner(
|
||||||
|
onSemanticsUpdate: (ui.SemanticsUpdate _) { assert(false); },
|
||||||
|
);
|
||||||
|
|
||||||
void setHitTest(BoxHitTest hitTest) {
|
void setHitTest(BoxHitTest hitTest) {
|
||||||
renderView.child = _TestHitTester(hitTest);
|
if (_pipelineOwner.rootNode == null) {
|
||||||
|
_pipelineOwner.rootNode = _renderView;
|
||||||
|
rootPipelineOwner.adoptChild(_pipelineOwner);
|
||||||
|
addRenderView(_renderView);
|
||||||
|
}
|
||||||
|
_renderView.child = _TestHitTester(hitTest);
|
||||||
}
|
}
|
||||||
|
|
||||||
SchedulerPhase? _overridePhase;
|
SchedulerPhase? _overridePhase;
|
||||||
|
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(root.semanticsOwner, isNotNull);
|
||||||
expect(child.semanticsOwner, isNotNull);
|
expect(child.semanticsOwner, isNotNull);
|
||||||
expect(childOfChild.semanticsOwner, isNull);
|
expect(childOfChild.semanticsOwner, isNotNull); // Retained in case we get re-attached.
|
||||||
|
|
||||||
final SemanticsHandle childSemantics = child.ensureSemantics();
|
final SemanticsHandle childSemantics = child.ensureSemantics();
|
||||||
root.dropChild(child);
|
root.dropChild(child);
|
||||||
|
|
||||||
expect(root.semanticsOwner, isNotNull);
|
expect(root.semanticsOwner, isNotNull);
|
||||||
expect(child.semanticsOwner, isNotNull);
|
expect(child.semanticsOwner, isNotNull);
|
||||||
expect(childOfChild.semanticsOwner, isNull);
|
expect(childOfChild.semanticsOwner, isNotNull); // Retained in case we get re-attached.
|
||||||
|
|
||||||
childSemantics.dispose();
|
childSemantics.dispose();
|
||||||
|
|
||||||
expect(root.semanticsOwner, isNotNull);
|
expect(root.semanticsOwner, isNotNull);
|
||||||
expect(child.semanticsOwner, isNull);
|
expect(child.semanticsOwner, isNull);
|
||||||
expect(childOfChild.semanticsOwner, isNull);
|
expect(childOfChild.semanticsOwner, isNotNull);
|
||||||
|
|
||||||
|
manifold.semanticsEnabled = false;
|
||||||
|
|
||||||
|
expect(root.semanticsOwner, isNull);
|
||||||
|
expect(childOfChild.semanticsOwner, isNotNull);
|
||||||
|
|
||||||
|
root.adoptChild(childOfChild);
|
||||||
|
expect(root.semanticsOwner, isNull);
|
||||||
|
expect(childOfChild.semanticsOwner, isNull); // Disposed on re-attachment.
|
||||||
|
|
||||||
|
manifold.semanticsEnabled = true;
|
||||||
|
expect(root.semanticsOwner, isNotNull);
|
||||||
|
expect(childOfChild.semanticsOwner, isNotNull);
|
||||||
|
|
||||||
|
root.dropChild(childOfChild);
|
||||||
|
|
||||||
|
expect(root.semanticsOwner, isNotNull);
|
||||||
|
expect(childOfChild.semanticsOwner, isNotNull);
|
||||||
|
|
||||||
|
childOfChild.dispose();
|
||||||
|
|
||||||
|
expect(root.semanticsOwner, isNotNull);
|
||||||
|
expect(childOfChild.semanticsOwner, isNull); // Disposed on dispose.
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can adopt/drop children during own layout', () {
|
test('can adopt/drop children during own layout', () {
|
||||||
@ -789,6 +812,38 @@ void main() {
|
|||||||
});
|
});
|
||||||
expect(children.single, childOfChild3);
|
expect(children.single, childOfChild3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('printing pipeline owner tree smoke test', () {
|
||||||
|
final PipelineOwner root = PipelineOwner();
|
||||||
|
final PipelineOwner child1 = PipelineOwner()
|
||||||
|
..rootNode = FakeRenderView();
|
||||||
|
final PipelineOwner childOfChild1 = PipelineOwner()
|
||||||
|
..rootNode = FakeRenderView();
|
||||||
|
final PipelineOwner child2 = PipelineOwner()
|
||||||
|
..rootNode = FakeRenderView();
|
||||||
|
final PipelineOwner childOfChild2 = PipelineOwner()
|
||||||
|
..rootNode = FakeRenderView();
|
||||||
|
|
||||||
|
root.adoptChild(child1);
|
||||||
|
child1.adoptChild(childOfChild1);
|
||||||
|
root.adoptChild(child2);
|
||||||
|
child2.adoptChild(childOfChild2);
|
||||||
|
|
||||||
|
expect(root.toStringDeep(), equalsIgnoringHashCodes(
|
||||||
|
'PipelineOwner#00000\n'
|
||||||
|
' ├─PipelineOwner#00000\n'
|
||||||
|
' │ │ rootNode: FakeRenderView#00000 NEEDS-LAYOUT NEEDS-PAINT\n'
|
||||||
|
' │ │\n'
|
||||||
|
' │ └─PipelineOwner#00000\n'
|
||||||
|
' │ rootNode: FakeRenderView#00000 NEEDS-LAYOUT NEEDS-PAINT\n'
|
||||||
|
' │\n'
|
||||||
|
' └─PipelineOwner#00000\n'
|
||||||
|
' │ rootNode: FakeRenderView#00000 NEEDS-LAYOUT NEEDS-PAINT\n'
|
||||||
|
' │\n'
|
||||||
|
' └─PipelineOwner#00000\n'
|
||||||
|
' rootNode: FakeRenderView#00000 NEEDS-LAYOUT NEEDS-PAINT\n'
|
||||||
|
));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestPipelineManifold extends ChangeNotifier implements PipelineManifold {
|
class TestPipelineManifold extends ChangeNotifier implements PipelineManifold {
|
||||||
@ -860,3 +915,5 @@ List<PipelineOwner> _treeWalk(PipelineOwner root) {
|
|||||||
root.visitChildren(visitor);
|
root.visitChildren(visitor);
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FakeRenderView extends RenderBox { }
|
||||||
|
@ -47,7 +47,7 @@ void main() {
|
|||||||
child: platformViewRenderBox,
|
child: platformViewRenderBox,
|
||||||
);
|
);
|
||||||
int semanticsUpdateCount = 0;
|
int semanticsUpdateCount = 0;
|
||||||
final SemanticsHandle semanticsHandle = TestRenderingFlutterBinding.instance.pipelineOwner.ensureSemantics(
|
final SemanticsHandle semanticsHandle = TestRenderingFlutterBinding.instance.rootPipelineOwner.ensureSemantics(
|
||||||
listener: () {
|
listener: () {
|
||||||
++semanticsUpdateCount;
|
++semanticsUpdateCount;
|
||||||
},
|
},
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:ui' show SemanticsUpdate;
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
@ -43,6 +44,44 @@ class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, Ser
|
|||||||
void initInstances() {
|
void initInstances() {
|
||||||
super.initInstances();
|
super.initInstances();
|
||||||
_instance = this;
|
_instance = this;
|
||||||
|
// TODO(goderbauer): Create (fake) window if embedder doesn't provide an implicit view.
|
||||||
|
assert(platformDispatcher.implicitView != null);
|
||||||
|
_renderView = initRenderView(platformDispatcher.implicitView!);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
RenderView get renderView => _renderView;
|
||||||
|
late RenderView _renderView;
|
||||||
|
|
||||||
|
@override
|
||||||
|
PipelineOwner get pipelineOwner => rootPipelineOwner;
|
||||||
|
|
||||||
|
/// Creates a [RenderView] object to be the root of the
|
||||||
|
/// [RenderObject] rendering tree, and initializes it so that it
|
||||||
|
/// will be rendered when the next frame is requested.
|
||||||
|
///
|
||||||
|
/// Called automatically when the binding is created.
|
||||||
|
RenderView initRenderView(FlutterView view) {
|
||||||
|
final RenderView renderView = RenderView(view: view);
|
||||||
|
rootPipelineOwner.rootNode = renderView;
|
||||||
|
addRenderView(renderView);
|
||||||
|
renderView.prepareInitialFrame();
|
||||||
|
return renderView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
PipelineOwner createRootPipelineOwner() {
|
||||||
|
return PipelineOwner(
|
||||||
|
onSemanticsOwnerCreated: () {
|
||||||
|
renderView.scheduleInitialSemantics();
|
||||||
|
},
|
||||||
|
onSemanticsUpdate: (SemanticsUpdate update) {
|
||||||
|
renderView.updateSemantics(update);
|
||||||
|
},
|
||||||
|
onSemanticsOwnerDisposed: () {
|
||||||
|
renderView.clearSemantics();
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates and initializes the binding. This function is
|
/// Creates and initializes the binding. This function is
|
||||||
@ -139,23 +178,25 @@ class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, Ser
|
|||||||
final FlutterExceptionHandler? oldErrorHandler = FlutterError.onError;
|
final FlutterExceptionHandler? oldErrorHandler = FlutterError.onError;
|
||||||
FlutterError.onError = _errors.add;
|
FlutterError.onError = _errors.add;
|
||||||
try {
|
try {
|
||||||
pipelineOwner.flushLayout();
|
rootPipelineOwner.flushLayout();
|
||||||
if (phase == EnginePhase.layout) {
|
if (phase == EnginePhase.layout) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pipelineOwner.flushCompositingBits();
|
rootPipelineOwner.flushCompositingBits();
|
||||||
if (phase == EnginePhase.compositingBits) {
|
if (phase == EnginePhase.compositingBits) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pipelineOwner.flushPaint();
|
rootPipelineOwner.flushPaint();
|
||||||
if (phase == EnginePhase.paint) {
|
if (phase == EnginePhase.paint) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
for (final RenderView renderView in renderViews) {
|
||||||
renderView.compositeFrame();
|
renderView.compositeFrame();
|
||||||
|
}
|
||||||
if (phase == EnginePhase.composite) {
|
if (phase == EnginePhase.composite) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pipelineOwner.flushSemantics();
|
rootPipelineOwner.flushSemantics();
|
||||||
if (phase == EnginePhase.flushSemantics) {
|
if (phase == EnginePhase.flushSemantics) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -122,6 +122,16 @@ void main() {
|
|||||||
isNot(paintsGreenRect),
|
isNot(paintsGreenRect),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Config can be set and changed after instantiation without calling prepareInitialFrame first', () {
|
||||||
|
final RenderView view = RenderView(
|
||||||
|
view: RendererBinding.instance.platformDispatcher.views.single,
|
||||||
|
);
|
||||||
|
view.configuration = const ViewConfiguration(size: Size(100, 200), devicePixelRatio: 3.0);
|
||||||
|
view.configuration = const ViewConfiguration(size: Size(200, 300), devicePixelRatio: 2.0);
|
||||||
|
PipelineOwner().rootNode = view;
|
||||||
|
view.prepareInitialFrame();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const Color orange = Color(0xFFFF9000);
|
const Color orange = Color(0xFFFF9000);
|
||||||
|
@ -42,7 +42,7 @@ void main() {
|
|||||||
await benchmarkWidgets(
|
await benchmarkWidgets(
|
||||||
(WidgetTester tester) async {
|
(WidgetTester tester) async {
|
||||||
const Key root = Key('root');
|
const Key root = Key('root');
|
||||||
binding.attachRootWidget(Container(key: root));
|
binding.attachRootWidget(binding.wrapWithDefaultView(Container(key: root)));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(binding.framesBegun, greaterThan(0));
|
expect(binding.framesBegun, greaterThan(0));
|
||||||
|
@ -156,8 +156,9 @@ void main() {
|
|||||||
equalsIgnoringHashCodes(
|
equalsIgnoringHashCodes(
|
||||||
'RenderPadding#00000 relayoutBoundary=up1\n'
|
'RenderPadding#00000 relayoutBoundary=up1\n'
|
||||||
' │ creator: Padding ← Container ← Align ← MediaQuery ←\n'
|
' │ creator: Padding ← Container ← Align ← MediaQuery ←\n'
|
||||||
' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n'
|
' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n'
|
||||||
' │ TestFlutterView#00000] ← [root]\n'
|
' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
|
||||||
|
' │ [root]\n'
|
||||||
' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
|
' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
|
||||||
' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n'
|
' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n'
|
||||||
' │ size: Size(63.0, 88.0)\n'
|
' │ size: Size(63.0, 88.0)\n'
|
||||||
@ -165,8 +166,9 @@ void main() {
|
|||||||
' │\n'
|
' │\n'
|
||||||
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
|
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
|
||||||
' │ creator: ConstrainedBox ← Padding ← Container ← Align ←\n'
|
' │ creator: ConstrainedBox ← Padding ← Container ← Align ←\n'
|
||||||
' │ MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n'
|
' │ MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope ←\n'
|
||||||
' │ View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n'
|
' │ _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
|
||||||
|
' │ TestFlutterView#00000] ← View ← [root]\n'
|
||||||
' │ parentData: offset=Offset(5.0, 5.0) (can use size)\n'
|
' │ parentData: offset=Offset(5.0, 5.0) (can use size)\n'
|
||||||
' │ constraints: BoxConstraints(0.0<=w<=790.0, 0.0<=h<=590.0)\n'
|
' │ constraints: BoxConstraints(0.0<=w<=790.0, 0.0<=h<=590.0)\n'
|
||||||
' │ size: Size(53.0, 78.0)\n'
|
' │ size: Size(53.0, 78.0)\n'
|
||||||
@ -174,8 +176,9 @@ void main() {
|
|||||||
' │\n'
|
' │\n'
|
||||||
' └─child: RenderDecoratedBox#00000\n'
|
' └─child: RenderDecoratedBox#00000\n'
|
||||||
' │ creator: DecoratedBox ← ConstrainedBox ← Padding ← Container ←\n'
|
' │ creator: DecoratedBox ← ConstrainedBox ← Padding ← Container ←\n'
|
||||||
' │ Align ← MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n'
|
' │ Align ← MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope\n'
|
||||||
' │ View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n'
|
' │ ← _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
|
||||||
|
' │ TestFlutterView#00000] ← View ← [root]\n'
|
||||||
' │ parentData: <none> (can use size)\n'
|
' │ parentData: <none> (can use size)\n'
|
||||||
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
||||||
' │ size: Size(53.0, 78.0)\n'
|
' │ size: Size(53.0, 78.0)\n'
|
||||||
@ -188,8 +191,9 @@ void main() {
|
|||||||
' └─child: _RenderColoredBox#00000\n'
|
' └─child: _RenderColoredBox#00000\n'
|
||||||
' │ creator: ColoredBox ← DecoratedBox ← ConstrainedBox ← Padding ←\n'
|
' │ creator: ColoredBox ← DecoratedBox ← ConstrainedBox ← Padding ←\n'
|
||||||
' │ Container ← Align ← MediaQuery ← _MediaQueryFromView ←\n'
|
' │ Container ← Align ← MediaQuery ← _MediaQueryFromView ←\n'
|
||||||
' │ _ViewScope ← View-[GlobalObjectKey TestFlutterView#00000] ←\n'
|
' │ _PipelineOwnerScope ← _ViewScope ←\n'
|
||||||
' │ [root]\n'
|
' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
|
||||||
|
' │ ⋯\n'
|
||||||
' │ parentData: <none> (can use size)\n'
|
' │ parentData: <none> (can use size)\n'
|
||||||
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
||||||
' │ size: Size(53.0, 78.0)\n'
|
' │ size: Size(53.0, 78.0)\n'
|
||||||
@ -198,8 +202,8 @@ void main() {
|
|||||||
' └─child: RenderPadding#00000\n'
|
' └─child: RenderPadding#00000\n'
|
||||||
' │ creator: Padding ← ColoredBox ← DecoratedBox ← ConstrainedBox ←\n'
|
' │ creator: Padding ← ColoredBox ← DecoratedBox ← ConstrainedBox ←\n'
|
||||||
' │ Padding ← Container ← Align ← MediaQuery ← _MediaQueryFromView\n'
|
' │ Padding ← Container ← Align ← MediaQuery ← _MediaQueryFromView\n'
|
||||||
' │ ← _ViewScope ← View-[GlobalObjectKey TestFlutterView#00000] ←\n'
|
' │ ← _PipelineOwnerScope ← _ViewScope ←\n'
|
||||||
' │ [root]\n'
|
' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← ⋯\n'
|
||||||
' │ parentData: <none> (can use size)\n'
|
' │ parentData: <none> (can use size)\n'
|
||||||
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
||||||
' │ size: Size(53.0, 78.0)\n'
|
' │ size: Size(53.0, 78.0)\n'
|
||||||
@ -208,8 +212,7 @@ void main() {
|
|||||||
' └─child: RenderPositionedBox#00000\n'
|
' └─child: RenderPositionedBox#00000\n'
|
||||||
' │ creator: Align ← Padding ← ColoredBox ← DecoratedBox ←\n'
|
' │ creator: Align ← Padding ← ColoredBox ← DecoratedBox ←\n'
|
||||||
' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n'
|
' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n'
|
||||||
' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n'
|
' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ← ⋯\n'
|
||||||
' │ TestFlutterView#00000] ← ⋯\n'
|
|
||||||
' │ parentData: offset=Offset(7.0, 7.0) (can use size)\n'
|
' │ parentData: offset=Offset(7.0, 7.0) (can use size)\n'
|
||||||
' │ constraints: BoxConstraints(w=39.0, h=64.0)\n'
|
' │ constraints: BoxConstraints(w=39.0, h=64.0)\n'
|
||||||
' │ size: Size(39.0, 64.0)\n'
|
' │ size: Size(39.0, 64.0)\n'
|
||||||
@ -220,7 +223,7 @@ void main() {
|
|||||||
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up1\n'
|
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up1\n'
|
||||||
' │ creator: SizedBox ← Align ← Padding ← ColoredBox ← DecoratedBox ←\n'
|
' │ creator: SizedBox ← Align ← Padding ← ColoredBox ← DecoratedBox ←\n'
|
||||||
' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n'
|
' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n'
|
||||||
' │ _MediaQueryFromView ← _ViewScope ← ⋯\n'
|
' │ _MediaQueryFromView ← _PipelineOwnerScope ← ⋯\n'
|
||||||
' │ parentData: offset=Offset(14.0, 31.0) (can use size)\n'
|
' │ parentData: offset=Offset(14.0, 31.0) (can use size)\n'
|
||||||
' │ constraints: BoxConstraints(0.0<=w<=39.0, 0.0<=h<=64.0)\n'
|
' │ constraints: BoxConstraints(0.0<=w<=39.0, 0.0<=h<=64.0)\n'
|
||||||
' │ size: Size(25.0, 33.0)\n'
|
' │ size: Size(25.0, 33.0)\n'
|
||||||
@ -255,8 +258,9 @@ void main() {
|
|||||||
equalsIgnoringHashCodes(
|
equalsIgnoringHashCodes(
|
||||||
'RenderPadding#00000 relayoutBoundary=up1\n'
|
'RenderPadding#00000 relayoutBoundary=up1\n'
|
||||||
' │ creator: Padding ← Container ← Align ← MediaQuery ←\n'
|
' │ creator: Padding ← Container ← Align ← MediaQuery ←\n'
|
||||||
' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n'
|
' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n'
|
||||||
' │ TestFlutterView#00000] ← [root]\n'
|
' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
|
||||||
|
' │ [root]\n'
|
||||||
' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
|
' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
|
||||||
' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n'
|
' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n'
|
||||||
' │ layer: null\n'
|
' │ layer: null\n'
|
||||||
@ -267,8 +271,9 @@ void main() {
|
|||||||
' │\n'
|
' │\n'
|
||||||
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
|
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
|
||||||
' │ creator: ConstrainedBox ← Padding ← Container ← Align ←\n'
|
' │ creator: ConstrainedBox ← Padding ← Container ← Align ←\n'
|
||||||
' │ MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n'
|
' │ MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope ←\n'
|
||||||
' │ View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n'
|
' │ _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
|
||||||
|
' │ TestFlutterView#00000] ← View ← [root]\n'
|
||||||
' │ parentData: offset=Offset(5.0, 5.0) (can use size)\n'
|
' │ parentData: offset=Offset(5.0, 5.0) (can use size)\n'
|
||||||
' │ constraints: BoxConstraints(0.0<=w<=790.0, 0.0<=h<=590.0)\n'
|
' │ constraints: BoxConstraints(0.0<=w<=790.0, 0.0<=h<=590.0)\n'
|
||||||
' │ layer: null\n'
|
' │ layer: null\n'
|
||||||
@ -278,8 +283,9 @@ void main() {
|
|||||||
' │\n'
|
' │\n'
|
||||||
' └─child: RenderDecoratedBox#00000\n'
|
' └─child: RenderDecoratedBox#00000\n'
|
||||||
' │ creator: DecoratedBox ← ConstrainedBox ← Padding ← Container ←\n'
|
' │ creator: DecoratedBox ← ConstrainedBox ← Padding ← Container ←\n'
|
||||||
' │ Align ← MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n'
|
' │ Align ← MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope\n'
|
||||||
' │ View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n'
|
' │ ← _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
|
||||||
|
' │ TestFlutterView#00000] ← View ← [root]\n'
|
||||||
' │ parentData: <none> (can use size)\n'
|
' │ parentData: <none> (can use size)\n'
|
||||||
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
||||||
' │ layer: null\n'
|
' │ layer: null\n'
|
||||||
@ -300,8 +306,9 @@ void main() {
|
|||||||
' └─child: _RenderColoredBox#00000\n'
|
' └─child: _RenderColoredBox#00000\n'
|
||||||
' │ creator: ColoredBox ← DecoratedBox ← ConstrainedBox ← Padding ←\n'
|
' │ creator: ColoredBox ← DecoratedBox ← ConstrainedBox ← Padding ←\n'
|
||||||
' │ Container ← Align ← MediaQuery ← _MediaQueryFromView ←\n'
|
' │ Container ← Align ← MediaQuery ← _MediaQueryFromView ←\n'
|
||||||
' │ _ViewScope ← View-[GlobalObjectKey TestFlutterView#00000] ←\n'
|
' │ _PipelineOwnerScope ← _ViewScope ←\n'
|
||||||
' │ [root]\n'
|
' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
|
||||||
|
' │ ⋯\n'
|
||||||
' │ parentData: <none> (can use size)\n'
|
' │ parentData: <none> (can use size)\n'
|
||||||
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
||||||
' │ layer: null\n'
|
' │ layer: null\n'
|
||||||
@ -312,8 +319,8 @@ void main() {
|
|||||||
' └─child: RenderPadding#00000\n'
|
' └─child: RenderPadding#00000\n'
|
||||||
' │ creator: Padding ← ColoredBox ← DecoratedBox ← ConstrainedBox ←\n'
|
' │ creator: Padding ← ColoredBox ← DecoratedBox ← ConstrainedBox ←\n'
|
||||||
' │ Padding ← Container ← Align ← MediaQuery ← _MediaQueryFromView\n'
|
' │ Padding ← Container ← Align ← MediaQuery ← _MediaQueryFromView\n'
|
||||||
' │ ← _ViewScope ← View-[GlobalObjectKey TestFlutterView#00000] ←\n'
|
' │ ← _PipelineOwnerScope ← _ViewScope ←\n'
|
||||||
' │ [root]\n'
|
' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← ⋯\n'
|
||||||
' │ parentData: <none> (can use size)\n'
|
' │ parentData: <none> (can use size)\n'
|
||||||
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
||||||
' │ layer: null\n'
|
' │ layer: null\n'
|
||||||
@ -325,8 +332,7 @@ void main() {
|
|||||||
' └─child: RenderPositionedBox#00000\n'
|
' └─child: RenderPositionedBox#00000\n'
|
||||||
' │ creator: Align ← Padding ← ColoredBox ← DecoratedBox ←\n'
|
' │ creator: Align ← Padding ← ColoredBox ← DecoratedBox ←\n'
|
||||||
' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n'
|
' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n'
|
||||||
' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n'
|
' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ← ⋯\n'
|
||||||
' │ TestFlutterView#00000] ← ⋯\n'
|
|
||||||
' │ parentData: offset=Offset(7.0, 7.0) (can use size)\n'
|
' │ parentData: offset=Offset(7.0, 7.0) (can use size)\n'
|
||||||
' │ constraints: BoxConstraints(w=39.0, h=64.0)\n'
|
' │ constraints: BoxConstraints(w=39.0, h=64.0)\n'
|
||||||
' │ layer: null\n'
|
' │ layer: null\n'
|
||||||
@ -340,7 +346,7 @@ void main() {
|
|||||||
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up1\n'
|
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up1\n'
|
||||||
' │ creator: SizedBox ← Align ← Padding ← ColoredBox ← DecoratedBox ←\n'
|
' │ creator: SizedBox ← Align ← Padding ← ColoredBox ← DecoratedBox ←\n'
|
||||||
' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n'
|
' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n'
|
||||||
' │ _MediaQueryFromView ← _ViewScope ← ⋯\n'
|
' │ _MediaQueryFromView ← _PipelineOwnerScope ← ⋯\n'
|
||||||
' │ parentData: offset=Offset(14.0, 31.0) (can use size)\n'
|
' │ parentData: offset=Offset(14.0, 31.0) (can use size)\n'
|
||||||
' │ constraints: BoxConstraints(0.0<=w<=39.0, 0.0<=h<=64.0)\n'
|
' │ constraints: BoxConstraints(0.0<=w<=39.0, 0.0<=h<=64.0)\n'
|
||||||
' │ layer: null\n'
|
' │ layer: null\n'
|
||||||
@ -367,7 +373,7 @@ void main() {
|
|||||||
' shape: rectangle\n'
|
' shape: rectangle\n'
|
||||||
' configuration: ImageConfiguration(bundle:\n'
|
' configuration: ImageConfiguration(bundle:\n'
|
||||||
' PlatformAssetBundle#00000(), devicePixelRatio: 3.0, platform:\n'
|
' PlatformAssetBundle#00000(), devicePixelRatio: 3.0, platform:\n'
|
||||||
' android)\n',
|
' android)\n'
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -386,8 +392,9 @@ void main() {
|
|||||||
'RenderPadding#00000 relayoutBoundary=up1\n'
|
'RenderPadding#00000 relayoutBoundary=up1\n'
|
||||||
' │ needsCompositing: false\n'
|
' │ needsCompositing: false\n'
|
||||||
' │ creator: Padding ← Container ← Align ← MediaQuery ←\n'
|
' │ creator: Padding ← Container ← Align ← MediaQuery ←\n'
|
||||||
' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n'
|
' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n'
|
||||||
' │ TestFlutterView#00000] ← [root]\n'
|
' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
|
||||||
|
' │ [root]\n'
|
||||||
' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
|
' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
|
||||||
' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n'
|
' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n'
|
||||||
' │ layer: null\n'
|
' │ layer: null\n'
|
||||||
@ -401,8 +408,9 @@ void main() {
|
|||||||
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
|
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
|
||||||
' │ needsCompositing: false\n'
|
' │ needsCompositing: false\n'
|
||||||
' │ creator: ConstrainedBox ← Padding ← Container ← Align ←\n'
|
' │ creator: ConstrainedBox ← Padding ← Container ← Align ←\n'
|
||||||
' │ MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n'
|
' │ MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope ←\n'
|
||||||
' │ View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n'
|
' │ _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
|
||||||
|
' │ TestFlutterView#00000] ← View ← [root]\n'
|
||||||
' │ parentData: offset=Offset(5.0, 5.0) (can use size)\n'
|
' │ parentData: offset=Offset(5.0, 5.0) (can use size)\n'
|
||||||
' │ constraints: BoxConstraints(0.0<=w<=790.0, 0.0<=h<=590.0)\n'
|
' │ constraints: BoxConstraints(0.0<=w<=790.0, 0.0<=h<=590.0)\n'
|
||||||
' │ layer: null\n'
|
' │ layer: null\n'
|
||||||
@ -415,8 +423,9 @@ void main() {
|
|||||||
' └─child: RenderDecoratedBox#00000\n'
|
' └─child: RenderDecoratedBox#00000\n'
|
||||||
' │ needsCompositing: false\n'
|
' │ needsCompositing: false\n'
|
||||||
' │ creator: DecoratedBox ← ConstrainedBox ← Padding ← Container ←\n'
|
' │ creator: DecoratedBox ← ConstrainedBox ← Padding ← Container ←\n'
|
||||||
' │ Align ← MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n'
|
' │ Align ← MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope\n'
|
||||||
' │ View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n'
|
' │ ← _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
|
||||||
|
' │ TestFlutterView#00000] ← View ← [root]\n'
|
||||||
' │ parentData: <none> (can use size)\n'
|
' │ parentData: <none> (can use size)\n'
|
||||||
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
||||||
' │ layer: null\n'
|
' │ layer: null\n'
|
||||||
@ -440,8 +449,9 @@ void main() {
|
|||||||
' │ needsCompositing: false\n'
|
' │ needsCompositing: false\n'
|
||||||
' │ creator: ColoredBox ← DecoratedBox ← ConstrainedBox ← Padding ←\n'
|
' │ creator: ColoredBox ← DecoratedBox ← ConstrainedBox ← Padding ←\n'
|
||||||
' │ Container ← Align ← MediaQuery ← _MediaQueryFromView ←\n'
|
' │ Container ← Align ← MediaQuery ← _MediaQueryFromView ←\n'
|
||||||
' │ _ViewScope ← View-[GlobalObjectKey TestFlutterView#00000] ←\n'
|
' │ _PipelineOwnerScope ← _ViewScope ←\n'
|
||||||
' │ [root]\n'
|
' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
|
||||||
|
' │ ⋯\n'
|
||||||
' │ parentData: <none> (can use size)\n'
|
' │ parentData: <none> (can use size)\n'
|
||||||
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
||||||
' │ layer: null\n'
|
' │ layer: null\n'
|
||||||
@ -455,8 +465,8 @@ void main() {
|
|||||||
' │ needsCompositing: false\n'
|
' │ needsCompositing: false\n'
|
||||||
' │ creator: Padding ← ColoredBox ← DecoratedBox ← ConstrainedBox ←\n'
|
' │ creator: Padding ← ColoredBox ← DecoratedBox ← ConstrainedBox ←\n'
|
||||||
' │ Padding ← Container ← Align ← MediaQuery ← _MediaQueryFromView\n'
|
' │ Padding ← Container ← Align ← MediaQuery ← _MediaQueryFromView\n'
|
||||||
' │ ← _ViewScope ← View-[GlobalObjectKey TestFlutterView#00000] ←\n'
|
' │ ← _PipelineOwnerScope ← _ViewScope ←\n'
|
||||||
' │ [root]\n'
|
' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← ⋯\n'
|
||||||
' │ parentData: <none> (can use size)\n'
|
' │ parentData: <none> (can use size)\n'
|
||||||
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
' │ constraints: BoxConstraints(w=53.0, h=78.0)\n'
|
||||||
' │ layer: null\n'
|
' │ layer: null\n'
|
||||||
@ -471,8 +481,7 @@ void main() {
|
|||||||
' │ needsCompositing: false\n'
|
' │ needsCompositing: false\n'
|
||||||
' │ creator: Align ← Padding ← ColoredBox ← DecoratedBox ←\n'
|
' │ creator: Align ← Padding ← ColoredBox ← DecoratedBox ←\n'
|
||||||
' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n'
|
' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n'
|
||||||
' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n'
|
' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ← ⋯\n'
|
||||||
' │ TestFlutterView#00000] ← ⋯\n'
|
|
||||||
' │ parentData: offset=Offset(7.0, 7.0) (can use size)\n'
|
' │ parentData: offset=Offset(7.0, 7.0) (can use size)\n'
|
||||||
' │ constraints: BoxConstraints(w=39.0, h=64.0)\n'
|
' │ constraints: BoxConstraints(w=39.0, h=64.0)\n'
|
||||||
' │ layer: null\n'
|
' │ layer: null\n'
|
||||||
@ -489,7 +498,7 @@ void main() {
|
|||||||
' │ needsCompositing: false\n'
|
' │ needsCompositing: false\n'
|
||||||
' │ creator: SizedBox ← Align ← Padding ← ColoredBox ← DecoratedBox ←\n'
|
' │ creator: SizedBox ← Align ← Padding ← ColoredBox ← DecoratedBox ←\n'
|
||||||
' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n'
|
' │ ConstrainedBox ← Padding ← Container ← Align ← MediaQuery ←\n'
|
||||||
' │ _MediaQueryFromView ← _ViewScope ← ⋯\n'
|
' │ _MediaQueryFromView ← _PipelineOwnerScope ← ⋯\n'
|
||||||
' │ parentData: offset=Offset(14.0, 31.0) (can use size)\n'
|
' │ parentData: offset=Offset(14.0, 31.0) (can use size)\n'
|
||||||
' │ constraints: BoxConstraints(0.0<=w<=39.0, 0.0<=h<=64.0)\n'
|
' │ constraints: BoxConstraints(0.0<=w<=39.0, 0.0<=h<=64.0)\n'
|
||||||
' │ layer: null\n'
|
' │ layer: null\n'
|
||||||
@ -521,7 +530,7 @@ void main() {
|
|||||||
' shape: rectangle\n'
|
' shape: rectangle\n'
|
||||||
' configuration: ImageConfiguration(bundle:\n'
|
' configuration: ImageConfiguration(bundle:\n'
|
||||||
' PlatformAssetBundle#00000(), devicePixelRatio: 3.0, platform:\n'
|
' PlatformAssetBundle#00000(), devicePixelRatio: 3.0, platform:\n'
|
||||||
' android)\n',
|
' android)\n'
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -373,8 +373,9 @@ void main() {
|
|||||||
' The following child has no ID: RenderConstrainedBox#00000 NEEDS-LAYOUT NEEDS-PAINT:\n'
|
' The following child has no ID: RenderConstrainedBox#00000 NEEDS-LAYOUT NEEDS-PAINT:\n'
|
||||||
' creator: ConstrainedBox ← Container ← LayoutWithMissingId ←\n'
|
' creator: ConstrainedBox ← Container ← LayoutWithMissingId ←\n'
|
||||||
' CustomMultiChildLayout ← Center ← MediaQuery ←\n'
|
' CustomMultiChildLayout ← Center ← MediaQuery ←\n'
|
||||||
' _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n'
|
' _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n'
|
||||||
' TestFlutterView#00000] ← [root]\n'
|
' _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
|
||||||
|
' [root]\n'
|
||||||
' parentData: offset=Offset(0.0, 0.0); id=null\n'
|
' parentData: offset=Offset(0.0, 0.0); id=null\n'
|
||||||
' constraints: MISSING\n'
|
' constraints: MISSING\n'
|
||||||
' size: MISSING\n'
|
' size: MISSING\n'
|
||||||
|
@ -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'
|
'FocusManager#00000\n'
|
||||||
' │ primaryFocus: FocusNode#00000(Child 4 [PRIMARY FOCUS])\n'
|
' │ primaryFocus: FocusNode#00000(Child 4 [PRIMARY FOCUS])\n'
|
||||||
' │ primaryFocusCreator: Container-[GlobalKey#00000] ← MediaQuery ←\n'
|
' │ primaryFocusCreator: Container-[GlobalKey#00000] ← MediaQuery ←\n'
|
||||||
' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n'
|
' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n'
|
||||||
' │ TestFlutterView#00000] ← [root]\n'
|
' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
|
||||||
|
' │ [root]\n'
|
||||||
' │\n'
|
' │\n'
|
||||||
' └─rootScope: FocusScopeNode#00000(Root Focus Scope [IN FOCUS PATH])\n'
|
' └─rootScope: FocusScopeNode#00000(Root Focus Scope [IN FOCUS PATH])\n'
|
||||||
' │ IN FOCUS PATH\n'
|
' │ IN FOCUS PATH\n'
|
||||||
|
@ -30,7 +30,7 @@ class TestWidgetState extends State<TestWidget> {
|
|||||||
void main() {
|
void main() {
|
||||||
testWidgets('initState() is called when we are in the tree', (WidgetTester tester) async {
|
testWidgets('initState() is called when we are in the tree', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(const Parent(child: TestWidget()));
|
await tester.pumpWidget(const Parent(child: TestWidget()));
|
||||||
expect(ancestors, containsAllInOrder(<String>['Parent', 'View', 'RenderObjectToWidgetAdapter<RenderBox>']));
|
expect(ancestors, containsAllInOrder(<String>['Parent', 'View', 'RootWidget']));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,7 +205,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
// The important lines below are the ones marked with "<----"
|
// The important lines below are the ones marked with "<----"
|
||||||
expect(tester.binding.renderView.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes(
|
expect(tester.binding.renderView.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes(
|
||||||
'RenderView#00000\n'
|
'_ReusableRenderView#00000\n'
|
||||||
' │ debug mode enabled - ${Platform.operatingSystem}\n'
|
' │ debug mode enabled - ${Platform.operatingSystem}\n'
|
||||||
' │ view size: Size(2400.0, 1800.0) (in physical pixels)\n'
|
' │ view size: Size(2400.0, 1800.0) (in physical pixels)\n'
|
||||||
' │ device pixel ratio: 3.0 (physical pixels per logical pixel)\n'
|
' │ device pixel ratio: 3.0 (physical pixels per logical pixel)\n'
|
||||||
@ -379,7 +379,7 @@ void main() {
|
|||||||
await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0));
|
await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(tester.binding.renderView.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes(
|
expect(tester.binding.renderView.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes(
|
||||||
'RenderView#00000\n'
|
'_ReusableRenderView#00000\n'
|
||||||
' │ debug mode enabled - ${Platform.operatingSystem}\n'
|
' │ debug mode enabled - ${Platform.operatingSystem}\n'
|
||||||
' │ view size: Size(2400.0, 1800.0) (in physical pixels)\n'
|
' │ view size: Size(2400.0, 1800.0) (in physical pixels)\n'
|
||||||
' │ device pixel ratio: 3.0 (physical pixels per logical pixel)\n'
|
' │ device pixel ratio: 3.0 (physical pixels per logical pixel)\n'
|
||||||
|
@ -44,26 +44,25 @@ class _MediaQueryAspectVariant extends TestVariant<_MediaQueryAspectCase> {
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('MediaQuery does not have a default', (WidgetTester tester) async {
|
testWidgets('MediaQuery does not have a default', (WidgetTester tester) async {
|
||||||
bool tested = false;
|
late final FlutterError error;
|
||||||
// Cannot use tester.pumpWidget here because it wraps the widget in a View,
|
// Cannot use tester.pumpWidget here because it wraps the widget in a View,
|
||||||
// which introduces a MediaQuery ancestor.
|
// which introduces a MediaQuery ancestor.
|
||||||
await pumpWidgetWithoutViewWrapper(
|
await pumpWidgetWithoutViewWrapper(
|
||||||
tester: tester,
|
tester: tester,
|
||||||
widget: Builder(
|
widget: Builder(
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
tested = true;
|
try {
|
||||||
MediaQuery.of(context); // should throw
|
MediaQuery.of(context);
|
||||||
return Container();
|
} on FlutterError catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
return View(
|
||||||
|
view: tester.view,
|
||||||
|
child: const SizedBox(),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(tested, isTrue);
|
|
||||||
final dynamic exception = tester.takeException();
|
|
||||||
expect(exception, isNotNull);
|
|
||||||
expect(exception ,isFlutterError);
|
|
||||||
final FlutterError error = exception as FlutterError;
|
|
||||||
expect(error.diagnostics.length, 5);
|
|
||||||
expect(error.diagnostics.last, isA<ErrorHint>());
|
|
||||||
expect(
|
expect(
|
||||||
error.toStringDeep(),
|
error.toStringDeep(),
|
||||||
startsWith(
|
startsWith(
|
||||||
@ -119,7 +118,10 @@ void main() {
|
|||||||
final MediaQueryData? data = MediaQuery.maybeOf(context);
|
final MediaQueryData? data = MediaQuery.maybeOf(context);
|
||||||
expect(data, isNull);
|
expect(data, isNull);
|
||||||
tested = true;
|
tested = true;
|
||||||
return Container();
|
return View(
|
||||||
|
view: tester.view,
|
||||||
|
child: const SizedBox(),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -295,7 +297,10 @@ void main() {
|
|||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
data = MediaQuery.of(context);
|
data = MediaQuery.of(context);
|
||||||
return const Placeholder();
|
return View(
|
||||||
|
view: tester.view,
|
||||||
|
child: const SizedBox(),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -348,7 +353,10 @@ void main() {
|
|||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
rebuildCount++;
|
rebuildCount++;
|
||||||
data = MediaQuery.of(context);
|
data = MediaQuery.of(context);
|
||||||
return const Placeholder();
|
return View(
|
||||||
|
view: tester.view,
|
||||||
|
child: const SizedBox(),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
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(
|
equalsIgnoringHashCodes(
|
||||||
'_RenderDiagonal#00000 relayoutBoundary=up1\n'
|
'_RenderDiagonal#00000 relayoutBoundary=up1\n'
|
||||||
' │ creator: _Diagonal ← Align ← Directionality ← MediaQuery ←\n'
|
' │ creator: _Diagonal ← Align ← Directionality ← MediaQuery ←\n'
|
||||||
' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n'
|
' │ _MediaQueryFromView ← _PipelineOwnerScope ← _ViewScope ←\n'
|
||||||
' │ TestFlutterView#00000] ← [root]\n'
|
' │ _RawView-[_DeprecatedRawViewKey TestFlutterView#00000] ← View ←\n'
|
||||||
|
' │ [root]\n'
|
||||||
' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
|
' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
|
||||||
' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n'
|
' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n'
|
||||||
' │ size: Size(190.0, 220.0)\n'
|
' │ size: Size(190.0, 220.0)\n'
|
||||||
' │\n'
|
' │\n'
|
||||||
' ├─topLeft: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
|
' ├─topLeft: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
|
||||||
' │ creator: SizedBox ← _Diagonal ← Align ← Directionality ←\n'
|
' │ creator: SizedBox ← _Diagonal ← Align ← Directionality ←\n'
|
||||||
' │ MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n'
|
' │ MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope ←\n'
|
||||||
' │ View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n'
|
' │ _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
|
||||||
|
' │ TestFlutterView#00000] ← View ← [root]\n'
|
||||||
' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
|
' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
|
||||||
' │ constraints: BoxConstraints(unconstrained)\n'
|
' │ constraints: BoxConstraints(unconstrained)\n'
|
||||||
' │ size: Size(80.0, 100.0)\n'
|
' │ size: Size(80.0, 100.0)\n'
|
||||||
@ -239,8 +241,9 @@ void main() {
|
|||||||
' │\n'
|
' │\n'
|
||||||
' └─bottomRight: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
|
' └─bottomRight: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
|
||||||
' creator: SizedBox ← _Diagonal ← Align ← Directionality ←\n'
|
' creator: SizedBox ← _Diagonal ← Align ← Directionality ←\n'
|
||||||
' MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n'
|
' MediaQuery ← _MediaQueryFromView ← _PipelineOwnerScope ←\n'
|
||||||
' View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n'
|
' _ViewScope ← _RawView-[_DeprecatedRawViewKey\n'
|
||||||
|
' TestFlutterView#00000] ← View ← [root]\n'
|
||||||
' parentData: offset=Offset(80.0, 100.0) (can use size)\n'
|
' parentData: offset=Offset(80.0, 100.0) (can use size)\n'
|
||||||
' constraints: BoxConstraints(unconstrained)\n'
|
' constraints: BoxConstraints(unconstrained)\n'
|
||||||
' size: Size(110.0, 120.0)\n'
|
' size: Size(110.0, 120.0)\n'
|
||||||
|
@ -10,10 +10,9 @@ import 'test_widgets.dart';
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('Stateful widget smoke test', (WidgetTester tester) async {
|
testWidgets('Stateful widget smoke test', (WidgetTester tester) async {
|
||||||
|
|
||||||
void checkTree(BoxDecoration expectedDecoration) {
|
void checkTree(BoxDecoration expectedDecoration) {
|
||||||
final SingleChildRenderObjectElement element = tester.element(
|
final SingleChildRenderObjectElement element = tester.element(
|
||||||
find.byElementPredicate((Element element) => element is SingleChildRenderObjectElement),
|
find.byElementPredicate((Element element) => element is SingleChildRenderObjectElement && element.renderObject is! RenderView),
|
||||||
);
|
);
|
||||||
expect(element, isNotNull);
|
expect(element, isNotNull);
|
||||||
expect(element.renderObject, isA<RenderDecoratedBox>());
|
expect(element.renderObject, isA<RenderDecoratedBox>());
|
||||||
|
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 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@ -67,4 +68,436 @@ void main() {
|
|||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('child of view finds view, parentPipelineOwner, mediaQuery', (WidgetTester tester) async {
|
||||||
|
FlutterView? outsideView;
|
||||||
|
FlutterView? insideView;
|
||||||
|
PipelineOwner? outsideParent;
|
||||||
|
PipelineOwner? insideParent;
|
||||||
|
|
||||||
|
await pumpWidgetWithoutViewWrapper(
|
||||||
|
tester: tester,
|
||||||
|
widget: Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
outsideView = View.maybeOf(context);
|
||||||
|
outsideParent = View.pipelineOwnerOf(context);
|
||||||
|
return View(
|
||||||
|
view: tester.view,
|
||||||
|
child: Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
insideView = View.maybeOf(context);
|
||||||
|
insideParent = View.pipelineOwnerOf(context);
|
||||||
|
return const SizedBox();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(outsideView, isNull);
|
||||||
|
expect(insideView, equals(tester.view));
|
||||||
|
|
||||||
|
expect(outsideParent, isNotNull);
|
||||||
|
expect(insideParent, isNotNull);
|
||||||
|
expect(outsideParent, isNot(equals(insideParent)));
|
||||||
|
|
||||||
|
expect(outsideParent, tester.binding.rootPipelineOwner);
|
||||||
|
expect(insideParent, equals(tester.renderObject(find.byType(SizedBox)).owner));
|
||||||
|
|
||||||
|
final List<PipelineOwner> pipelineOwners = <PipelineOwner> [];
|
||||||
|
tester.binding.rootPipelineOwner.visitChildren((PipelineOwner child) {
|
||||||
|
pipelineOwners.add(child);
|
||||||
|
});
|
||||||
|
expect(pipelineOwners.single, equals(insideParent));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('cannot have multiple views with same FlutterView', (WidgetTester tester) async {
|
||||||
|
await pumpWidgetWithoutViewWrapper(
|
||||||
|
tester: tester,
|
||||||
|
widget: ViewCollection(
|
||||||
|
views: <Widget>[
|
||||||
|
View(
|
||||||
|
view: tester.view,
|
||||||
|
child: const SizedBox(),
|
||||||
|
),
|
||||||
|
View(
|
||||||
|
view: tester.view,
|
||||||
|
child: const SizedBox(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
tester.takeException(),
|
||||||
|
isFlutterError.having(
|
||||||
|
(FlutterError e) => e.message,
|
||||||
|
'message',
|
||||||
|
contains('Multiple widgets used the same GlobalKey'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('ViewCollection must have one view', (WidgetTester tester) async {
|
||||||
|
expect(() => ViewCollection(views: const <Widget>[]), throwsAssertionError);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('ViewAnchor.child does not see surrounding view', (WidgetTester tester) async {
|
||||||
|
FlutterView? inside;
|
||||||
|
FlutterView? outside;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
outside = View.maybeOf(context);
|
||||||
|
return ViewAnchor(
|
||||||
|
view: Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
inside = View.maybeOf(context);
|
||||||
|
return View(view: FakeView(tester.view), child: const SizedBox());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
child: const SizedBox(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(inside, isNull);
|
||||||
|
expect(outside, isNotNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('ViewAnchor layout order', (WidgetTester tester) async {
|
||||||
|
Finder findSpyWidget(int label) {
|
||||||
|
return find.byWidgetPredicate((Widget w) => w is SpyRenderWidget && w.label == label);
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<String> log = <String>[];
|
||||||
|
await tester.pumpWidget(
|
||||||
|
SpyRenderWidget(
|
||||||
|
label: 1,
|
||||||
|
log: log,
|
||||||
|
child: ViewAnchor(
|
||||||
|
view: View(
|
||||||
|
view: FakeView(tester.view),
|
||||||
|
child: SpyRenderWidget(label: 2, log: log),
|
||||||
|
),
|
||||||
|
child: SpyRenderWidget(label: 3, log: log),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
log.clear();
|
||||||
|
tester.renderObject(findSpyWidget(3)).markNeedsLayout();
|
||||||
|
tester.renderObject(findSpyWidget(2)).markNeedsLayout();
|
||||||
|
tester.renderObject(findSpyWidget(1)).markNeedsLayout();
|
||||||
|
await tester.pump();
|
||||||
|
expect(log, <String>['layout 1', 'layout 3', 'layout 2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('visitChildren of ViewAnchor visits both children', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ViewAnchor(
|
||||||
|
view: View(
|
||||||
|
view: FakeView(tester.view),
|
||||||
|
child: const ColoredBox(color: Colors.green),
|
||||||
|
),
|
||||||
|
child: const SizedBox(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final Element viewAnchorElement = tester.element(find.byElementPredicate((Element e) => e.runtimeType.toString() == '_MultiChildComponentElement'));
|
||||||
|
final List<Element> children = <Element>[];
|
||||||
|
viewAnchorElement.visitChildren((Element element) {
|
||||||
|
children.add(element);
|
||||||
|
});
|
||||||
|
expect(children, hasLength(2));
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const ViewAnchor(
|
||||||
|
child: SizedBox(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
children.clear();
|
||||||
|
viewAnchorElement.visitChildren((Element element) {
|
||||||
|
children.add(element);
|
||||||
|
});
|
||||||
|
expect(children, hasLength(1));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('visitChildren of ViewCollection visits all children', (WidgetTester tester) async {
|
||||||
|
await pumpWidgetWithoutViewWrapper(
|
||||||
|
tester: tester,
|
||||||
|
widget: ViewCollection(
|
||||||
|
views: <Widget>[
|
||||||
|
View(
|
||||||
|
view: tester.view,
|
||||||
|
child: const SizedBox(),
|
||||||
|
),
|
||||||
|
View(
|
||||||
|
view: FakeView(tester.view),
|
||||||
|
child: const SizedBox(),
|
||||||
|
),
|
||||||
|
View(
|
||||||
|
view: FakeView(tester.view, viewId: 423),
|
||||||
|
child: const SizedBox(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final Element viewAnchorElement = tester.element(find.byElementPredicate((Element e) => e.runtimeType.toString() == '_MultiChildComponentElement'));
|
||||||
|
final List<Element> children = <Element>[];
|
||||||
|
viewAnchorElement.visitChildren((Element element) {
|
||||||
|
children.add(element);
|
||||||
|
});
|
||||||
|
expect(children, hasLength(3));
|
||||||
|
|
||||||
|
await pumpWidgetWithoutViewWrapper(
|
||||||
|
tester: tester,
|
||||||
|
widget: ViewCollection(
|
||||||
|
views: <Widget>[
|
||||||
|
View(
|
||||||
|
view: tester.view,
|
||||||
|
child: const SizedBox(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
children.clear();
|
||||||
|
viewAnchorElement.visitChildren((Element element) {
|
||||||
|
children.add(element);
|
||||||
|
});
|
||||||
|
expect(children, hasLength(1));
|
||||||
|
});
|
||||||
|
|
||||||
|
group('renderObject getter', () {
|
||||||
|
testWidgets('ancestors of view see RenderView as renderObject', (WidgetTester tester) async {
|
||||||
|
late BuildContext builderContext;
|
||||||
|
await pumpWidgetWithoutViewWrapper(
|
||||||
|
tester: tester,
|
||||||
|
widget: Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
builderContext = context;
|
||||||
|
return View(
|
||||||
|
view: tester.view,
|
||||||
|
child: const SizedBox(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final RenderObject? renderObject = builderContext.findRenderObject();
|
||||||
|
expect(renderObject, isNotNull);
|
||||||
|
expect(renderObject, isA<RenderView>());
|
||||||
|
expect(renderObject, tester.renderObject(find.byType(View)));
|
||||||
|
expect(tester.element(find.byType(Builder)).renderObject, renderObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('ancestors of ViewCollection get null for renderObject', (WidgetTester tester) async {
|
||||||
|
late BuildContext builderContext;
|
||||||
|
await pumpWidgetWithoutViewWrapper(
|
||||||
|
tester: tester,
|
||||||
|
widget: Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
builderContext = context;
|
||||||
|
return ViewCollection(
|
||||||
|
views: <Widget>[
|
||||||
|
View(
|
||||||
|
view: tester.view,
|
||||||
|
child: const SizedBox(),
|
||||||
|
),
|
||||||
|
View(
|
||||||
|
view: FakeView(tester.view),
|
||||||
|
child: const SizedBox(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final RenderObject? renderObject = builderContext.findRenderObject();
|
||||||
|
expect(renderObject, isNull);
|
||||||
|
expect(tester.element(find.byType(Builder)).renderObject, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('ancestors of a ViewAnchor see the right RenderObject', (WidgetTester tester) async {
|
||||||
|
late BuildContext builderContext;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
builderContext = context;
|
||||||
|
return ViewAnchor(
|
||||||
|
view: View(
|
||||||
|
view: FakeView(tester.view),
|
||||||
|
child: const ColoredBox(color: Colors.green),
|
||||||
|
),
|
||||||
|
child: const SizedBox(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final RenderObject? renderObject = builderContext.findRenderObject();
|
||||||
|
expect(renderObject, isNotNull);
|
||||||
|
expect(renderObject, isA<RenderConstrainedBox>());
|
||||||
|
expect(renderObject, tester.renderObject(find.byType(SizedBox)));
|
||||||
|
expect(tester.element(find.byType(Builder)).renderObject, renderObject);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('correctly switches between view configurations', (WidgetTester tester) async {
|
||||||
|
await pumpWidgetWithoutViewWrapper(
|
||||||
|
tester: tester,
|
||||||
|
widget: View(
|
||||||
|
view: tester.view,
|
||||||
|
deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner,
|
||||||
|
deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView,
|
||||||
|
child: const SizedBox(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
RenderObject renderView = tester.renderObject(find.byType(View));
|
||||||
|
expect(renderView, same(tester.binding.renderView));
|
||||||
|
expect(renderView.owner, same(tester.binding.pipelineOwner));
|
||||||
|
expect(tester.renderObject(find.byType(SizedBox)).owner, same(tester.binding.pipelineOwner));
|
||||||
|
|
||||||
|
await pumpWidgetWithoutViewWrapper(
|
||||||
|
tester: tester,
|
||||||
|
widget: View(
|
||||||
|
view: tester.view,
|
||||||
|
child: const SizedBox(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
renderView = tester.renderObject(find.byType(View));
|
||||||
|
expect(renderView, isNot(same(tester.binding.renderView)));
|
||||||
|
expect(renderView.owner, isNot(same(tester.binding.pipelineOwner)));
|
||||||
|
expect(tester.renderObject(find.byType(SizedBox)).owner, isNot(same(tester.binding.pipelineOwner)));
|
||||||
|
|
||||||
|
await pumpWidgetWithoutViewWrapper(
|
||||||
|
tester: tester,
|
||||||
|
widget: View(
|
||||||
|
view: tester.view,
|
||||||
|
deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner,
|
||||||
|
deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView,
|
||||||
|
child: const SizedBox(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
renderView = tester.renderObject(find.byType(View));
|
||||||
|
expect(renderView, same(tester.binding.renderView));
|
||||||
|
expect(renderView.owner, same(tester.binding.pipelineOwner));
|
||||||
|
expect(tester.renderObject(find.byType(SizedBox)).owner, same(tester.binding.pipelineOwner));
|
||||||
|
|
||||||
|
expect(() => View(
|
||||||
|
view: tester.view,
|
||||||
|
deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner,
|
||||||
|
child: const SizedBox(),
|
||||||
|
), throwsAssertionError);
|
||||||
|
expect(() => View(
|
||||||
|
view: tester.view,
|
||||||
|
deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView,
|
||||||
|
child: const SizedBox(),
|
||||||
|
), throwsAssertionError);
|
||||||
|
expect(() => View(
|
||||||
|
view: FakeView(tester.view),
|
||||||
|
deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView,
|
||||||
|
deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner,
|
||||||
|
child: const SizedBox(),
|
||||||
|
), throwsAssertionError);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('attaches itself correctly', (WidgetTester tester) async {
|
||||||
|
final Key viewKey = UniqueKey();
|
||||||
|
late final PipelineOwner parentPipelineOwner;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ViewAnchor(
|
||||||
|
view: Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
parentPipelineOwner = View.pipelineOwnerOf(context);
|
||||||
|
return View(
|
||||||
|
key: viewKey,
|
||||||
|
view: FakeView(tester.view),
|
||||||
|
child: const SizedBox(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
child: const ColoredBox(color: Colors.green),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(parentPipelineOwner, isNot(RendererBinding.instance.rootPipelineOwner));
|
||||||
|
|
||||||
|
final RenderView rawView = tester.renderObject<RenderView>(find.byKey(viewKey));
|
||||||
|
expect(RendererBinding.instance.renderViews, contains(rawView));
|
||||||
|
|
||||||
|
final List<PipelineOwner> children = <PipelineOwner>[];
|
||||||
|
parentPipelineOwner.visitChildren((PipelineOwner child) {
|
||||||
|
children.add(child);
|
||||||
|
});
|
||||||
|
final PipelineOwner rawViewOwner = rawView.owner!;
|
||||||
|
expect(children, contains(rawViewOwner));
|
||||||
|
|
||||||
|
// Remove that View from the tree.
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const ViewAnchor(
|
||||||
|
child: ColoredBox(color: Colors.green),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(rawView.owner, isNull);
|
||||||
|
expect(RendererBinding.instance.renderViews, isNot(contains(rawView)));
|
||||||
|
children.clear();
|
||||||
|
parentPipelineOwner.visitChildren((PipelineOwner child) {
|
||||||
|
children.add(child);
|
||||||
|
});
|
||||||
|
expect(children, isNot(contains(rawViewOwner)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) {
|
||||||
|
tester.binding.attachRootWidget(widget);
|
||||||
|
tester.binding.scheduleFrame();
|
||||||
|
return tester.binding.pump();
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeView extends TestFlutterView{
|
||||||
|
FakeView(FlutterView view, { this.viewId = 100 }) : super(
|
||||||
|
view: view,
|
||||||
|
platformDispatcher: view.platformDispatcher as TestPlatformDispatcher,
|
||||||
|
display: view.display as TestDisplay,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final int viewId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SpyRenderWidget extends SizedBox {
|
||||||
|
const SpyRenderWidget({super.key, required this.label, required this.log, super.child});
|
||||||
|
|
||||||
|
final int label;
|
||||||
|
final List<String> log;
|
||||||
|
|
||||||
|
@override
|
||||||
|
RenderSpy createRenderObject(BuildContext context) {
|
||||||
|
return RenderSpy(
|
||||||
|
additionalConstraints: const BoxConstraints(),
|
||||||
|
label: label,
|
||||||
|
log: log,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateRenderObject(BuildContext context, RenderSpy renderObject) {
|
||||||
|
renderObject
|
||||||
|
..label = label
|
||||||
|
..log = log;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RenderSpy extends RenderConstrainedBox {
|
||||||
|
RenderSpy({required super.additionalConstraints, required this.label, required this.log});
|
||||||
|
|
||||||
|
int label;
|
||||||
|
List<String> log;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void performLayout() {
|
||||||
|
log.add('layout $label');
|
||||||
|
super.performLayout();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4509,7 +4509,6 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
|||||||
expect(result['parentData'], isNull);
|
expect(result['parentData'], isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
testWidgets('ext.flutter.inspector.getLayoutExplorerNode for RenderView',(WidgetTester tester) async {
|
testWidgets('ext.flutter.inspector.getLayoutExplorerNode for RenderView',(WidgetTester tester) async {
|
||||||
await pumpWidgetForLayoutExplorer(tester);
|
await pumpWidgetForLayoutExplorer(tester);
|
||||||
|
|
||||||
@ -4530,7 +4529,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
|||||||
|
|
||||||
final Map<String, Object?>? renderObject = result['renderObject'] as Map<String, Object?>?;
|
final Map<String, Object?>? renderObject = result['renderObject'] as Map<String, Object?>?;
|
||||||
expect(renderObject, isNotNull);
|
expect(renderObject, isNotNull);
|
||||||
expect(renderObject!['description'], startsWith('RenderView'));
|
expect(renderObject!['description'], contains('RenderView'));
|
||||||
|
|
||||||
expect(result['parentRenderElement'], isNull);
|
expect(result['parentRenderElement'], isNull);
|
||||||
expect(result['constraints'], isNull);
|
expect(result['constraints'], isNull);
|
||||||
|
@ -55,7 +55,7 @@ class _TestRenderObject extends RenderObject {
|
|||||||
Rect get semanticBounds => throw UnimplementedError();
|
Rect get semanticBounds => throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TestElement extends RenderObjectElement with RootElementMixin {
|
class _TestElement extends RenderTreeRootElement with RootElementMixin {
|
||||||
_TestElement(): super(_TestLeafRenderObjectWidget());
|
_TestElement(): super(_TestLeafRenderObjectWidget());
|
||||||
|
|
||||||
void makeInactive() {
|
void makeInactive() {
|
||||||
|
@ -187,11 +187,20 @@ mixin CommandHandlerFactory {
|
|||||||
Future<Health> _getHealth(Command command) async => const Health(HealthStatus.ok);
|
Future<Health> _getHealth(Command command) async => const Health(HealthStatus.ok);
|
||||||
|
|
||||||
Future<LayerTree> _getLayerTree(Command command) async {
|
Future<LayerTree> _getLayerTree(Command command) async {
|
||||||
return LayerTree(RendererBinding.instance.renderView.debugLayer?.toStringDeep());
|
final String trees = <String>[
|
||||||
|
for (final RenderView renderView in RendererBinding.instance.renderViews)
|
||||||
|
if (renderView.debugLayer != null)
|
||||||
|
renderView.debugLayer!.toStringDeep(),
|
||||||
|
].join('\n\n');
|
||||||
|
return LayerTree(trees.isNotEmpty ? trees : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<RenderTree> _getRenderTree(Command command) async {
|
Future<RenderTree> _getRenderTree(Command command) async {
|
||||||
return RenderTree(RendererBinding.instance.renderView.toStringDeep());
|
final String trees = <String>[
|
||||||
|
for (final RenderView renderView in RendererBinding.instance.renderViews)
|
||||||
|
renderView.toStringDeep(),
|
||||||
|
].join('\n\n');
|
||||||
|
return RenderTree(trees.isNotEmpty ? trees : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Result> _enterText(Command command) async {
|
Future<Result> _enterText(Command command) async {
|
||||||
|
@ -58,8 +58,8 @@ class MatchesGoldenFile extends AsyncMatcher {
|
|||||||
final RenderObject renderObject = _findRepaintBoundary(element);
|
final RenderObject renderObject = _findRepaintBoundary(element);
|
||||||
final Size size = renderObject.paintBounds.size;
|
final Size size = renderObject.paintBounds.size;
|
||||||
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance;
|
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance;
|
||||||
final Element e = binding.rootElement!;
|
|
||||||
final ui.FlutterView view = binding.platformDispatcher.implicitView!;
|
final ui.FlutterView view = binding.platformDispatcher.implicitView!;
|
||||||
|
final RenderView renderView = binding.renderViews.firstWhere((RenderView r) => r.flutterView == view);
|
||||||
|
|
||||||
// Unlike `flutter_tester`, we don't have the ability to render an element
|
// Unlike `flutter_tester`, we don't have the ability to render an element
|
||||||
// to an image directly. Instead, we will use `window.render()` to render
|
// to an image directly. Instead, we will use `window.render()` to render
|
||||||
@ -78,7 +78,7 @@ class MatchesGoldenFile extends AsyncMatcher {
|
|||||||
return ex.message;
|
return ex.message;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_renderElement(view, _findRepaintBoundary(e));
|
_renderElement(view, renderView);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,11 +131,10 @@ class MinimumTapTargetGuideline extends AccessibilityGuideline {
|
|||||||
@override
|
@override
|
||||||
FutureOr<Evaluation> evaluate(WidgetTester tester) {
|
FutureOr<Evaluation> evaluate(WidgetTester tester) {
|
||||||
Evaluation result = const Evaluation.pass();
|
Evaluation result = const Evaluation.pass();
|
||||||
for (final FlutterView view in tester.platformDispatcher.views) {
|
for (final RenderView view in tester.binding.renderViews) {
|
||||||
result += _traverse(
|
result += _traverse(
|
||||||
view,
|
view.flutterView,
|
||||||
// TODO(pdblasi-google): Get the specific semantics root for this view when available
|
view.owner!.semanticsOwner!.rootSemanticsNode!,
|
||||||
tester.binding.pipelineOwner.semanticsOwner!.rootSemanticsNode!,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,10 +238,8 @@ class LabeledTapTargetGuideline extends AccessibilityGuideline {
|
|||||||
FutureOr<Evaluation> evaluate(WidgetTester tester) {
|
FutureOr<Evaluation> evaluate(WidgetTester tester) {
|
||||||
Evaluation result = const Evaluation.pass();
|
Evaluation result = const Evaluation.pass();
|
||||||
|
|
||||||
// TODO(pdblasi-google): Use view to retrieve the appropriate root semantics node when available.
|
for (final RenderView view in tester.binding.renderViews) {
|
||||||
// ignore: unused_local_variable
|
result += _traverse(view.owner!.semanticsOwner!.rootSemanticsNode!);
|
||||||
for (final FlutterView view in tester.platformDispatcher.views) {
|
|
||||||
result += _traverse(tester.binding.pipelineOwner.semanticsOwner!.rootSemanticsNode!);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -318,9 +315,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
|
|||||||
@override
|
@override
|
||||||
Future<Evaluation> evaluate(WidgetTester tester) async {
|
Future<Evaluation> evaluate(WidgetTester tester) async {
|
||||||
Evaluation result = const Evaluation.pass();
|
Evaluation result = const Evaluation.pass();
|
||||||
for (final FlutterView view in tester.platformDispatcher.views) {
|
for (final RenderView renderView in tester.binding.renderViews) {
|
||||||
// TODO(pdblasi): This renderView will need to be retrieved from view when available.
|
|
||||||
final RenderView renderView = tester.binding.renderView;
|
|
||||||
final OffsetLayer layer = renderView.debugLayer! as OffsetLayer;
|
final OffsetLayer layer = renderView.debugLayer! as OffsetLayer;
|
||||||
final SemanticsNode root = renderView.owner!.semanticsOwner!.rootSemanticsNode!;
|
final SemanticsNode root = renderView.owner!.semanticsOwner!.rootSemanticsNode!;
|
||||||
|
|
||||||
@ -329,13 +324,13 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
|
|||||||
() async {
|
() async {
|
||||||
// Needs to be the same pixel ratio otherwise our dimensions won't match
|
// Needs to be the same pixel ratio otherwise our dimensions won't match
|
||||||
// the last transform layer.
|
// the last transform layer.
|
||||||
final double ratio = 1 / view.devicePixelRatio;
|
final double ratio = 1 / renderView.flutterView.devicePixelRatio;
|
||||||
image = await layer.toImage(renderView.paintBounds, pixelRatio: ratio);
|
image = await layer.toImage(renderView.paintBounds, pixelRatio: ratio);
|
||||||
return image.toByteData();
|
return image.toByteData();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
result += await _evaluateNode(root, tester, image, byteData!, view);
|
result += await _evaluateNode(root, tester, image, byteData!, renderView);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -346,7 +341,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
|
|||||||
WidgetTester tester,
|
WidgetTester tester,
|
||||||
ui.Image image,
|
ui.Image image,
|
||||||
ByteData byteData,
|
ByteData byteData,
|
||||||
FlutterView view,
|
RenderView renderView,
|
||||||
) async {
|
) async {
|
||||||
Evaluation result = const Evaluation.pass();
|
Evaluation result = const Evaluation.pass();
|
||||||
|
|
||||||
@ -368,7 +363,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
for (final SemanticsNode child in children) {
|
for (final SemanticsNode child in children) {
|
||||||
result += await _evaluateNode(child, tester, image, byteData, view);
|
result += await _evaluateNode(child, tester, image, byteData, renderView);
|
||||||
}
|
}
|
||||||
if (shouldSkipNode(data)) {
|
if (shouldSkipNode(data)) {
|
||||||
return result;
|
return result;
|
||||||
@ -376,7 +371,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
|
|||||||
final String text = data.label.isEmpty ? data.value : data.label;
|
final String text = data.label.isEmpty ? data.value : data.label;
|
||||||
final Iterable<Element> elements = find.text(text).hitTestable().evaluate();
|
final Iterable<Element> elements = find.text(text).hitTestable().evaluate();
|
||||||
for (final Element element in elements) {
|
for (final Element element in elements) {
|
||||||
result += await _evaluateElement(node, element, tester, image, byteData, view);
|
result += await _evaluateElement(node, element, tester, image, byteData, renderView);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -387,7 +382,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
|
|||||||
WidgetTester tester,
|
WidgetTester tester,
|
||||||
ui.Image image,
|
ui.Image image,
|
||||||
ByteData byteData,
|
ByteData byteData,
|
||||||
FlutterView view,
|
RenderView renderView,
|
||||||
) async {
|
) async {
|
||||||
// Look up inherited text properties to determine text size and weight.
|
// Look up inherited text properties to determine text size and weight.
|
||||||
late bool isBold;
|
late bool isBold;
|
||||||
@ -408,7 +403,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
|
|||||||
// not included in renderBox.getTransformTo(null). Manually multiply the
|
// not included in renderBox.getTransformTo(null). Manually multiply the
|
||||||
// root transform to the global transform.
|
// root transform to the global transform.
|
||||||
final Matrix4 rootTransform = Matrix4.identity();
|
final Matrix4 rootTransform = Matrix4.identity();
|
||||||
tester.binding.renderView.applyPaintTransform(tester.binding.renderView.child!, rootTransform);
|
renderView.applyPaintTransform(renderView.child!, rootTransform);
|
||||||
rootTransform.multiply(globalTransform);
|
rootTransform.multiply(globalTransform);
|
||||||
screenBounds = MatrixUtils.transformRect(rootTransform, renderBox.paintBounds);
|
screenBounds = MatrixUtils.transformRect(rootTransform, renderBox.paintBounds);
|
||||||
Rect nodeBounds = node.rect;
|
Rect nodeBounds = node.rect;
|
||||||
@ -443,7 +438,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
|
|||||||
throw StateError('Unexpected widget type: ${widget.runtimeType}');
|
throw StateError('Unexpected widget type: ${widget.runtimeType}');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNodeOffScreen(paintBoundsWithOffset, view)) {
|
if (isNodeOffScreen(paintBoundsWithOffset, renderView.flutterView)) {
|
||||||
return const Evaluation.pass();
|
return const Evaluation.pass();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -562,9 +557,7 @@ class CustomMinimumContrastGuideline extends AccessibilityGuideline {
|
|||||||
Evaluation result = const Evaluation.pass();
|
Evaluation result = const Evaluation.pass();
|
||||||
for (final Element element in elements) {
|
for (final Element element in elements) {
|
||||||
final FlutterView view = tester.viewOf(find.byElementPredicate((Element e) => e == element));
|
final FlutterView view = tester.viewOf(find.byElementPredicate((Element e) => e == element));
|
||||||
|
final RenderView renderView = tester.binding.renderViews.firstWhere((RenderView r) => r.flutterView == view);
|
||||||
// TODO(pdblasi): Obtain this renderView from view when possible.
|
|
||||||
final RenderView renderView = tester.binding.renderView;
|
|
||||||
final OffsetLayer layer = renderView.debugLayer! as OffsetLayer;
|
final OffsetLayer layer = renderView.debugLayer! as OffsetLayer;
|
||||||
|
|
||||||
late final ui.Image image;
|
late final ui.Image image;
|
||||||
|
@ -495,8 +495,8 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
|
|||||||
|
|
||||||
Size? _surfaceSize;
|
Size? _surfaceSize;
|
||||||
|
|
||||||
/// Artificially changes the surface size to `size` on the Widget binding,
|
/// Artificially changes the logical size of [WidgetTester.view] to the
|
||||||
/// then flushes microtasks.
|
/// specified size, then flushes microtasks.
|
||||||
///
|
///
|
||||||
/// Set to null to use the default surface size.
|
/// Set to null to use the default surface size.
|
||||||
///
|
///
|
||||||
@ -508,7 +508,10 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
|
|||||||
/// addTearDown(() => binding.setSurfaceSize(null));
|
/// addTearDown(() => binding.setSurfaceSize(null));
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// See also [TestFlutterView.physicalSize], which has a similar effect.
|
/// This method only affects the size of the [WidgetTester.view]. It does not
|
||||||
|
/// affect the size of any other views. Instead of this method, consider
|
||||||
|
/// setting [TestFlutterView.physicalSize], which works for any view,
|
||||||
|
/// including [WidgetTester.view].
|
||||||
// TODO(pdblasi-google): Deprecate this. https://github.com/flutter/flutter/issues/123881
|
// TODO(pdblasi-google): Deprecate this. https://github.com/flutter/flutter/issues/123881
|
||||||
Future<void> setSurfaceSize(Size? size) {
|
Future<void> setSurfaceSize(Size? size) {
|
||||||
return TestAsyncUtils.guard<void>(() async {
|
return TestAsyncUtils.guard<void>(() async {
|
||||||
@ -522,15 +525,38 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ViewConfiguration createViewConfiguration() {
|
void addRenderView(RenderView view) {
|
||||||
final FlutterView view = platformDispatcher.implicitView!;
|
_insideAddRenderView = true;
|
||||||
final double devicePixelRatio = view.devicePixelRatio;
|
try {
|
||||||
final Size size = _surfaceSize ?? view.physicalSize / devicePixelRatio;
|
super.addRenderView(view);
|
||||||
|
} finally {
|
||||||
|
_insideAddRenderView = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _insideAddRenderView = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ViewConfiguration createViewConfigurationFor(RenderView renderView) {
|
||||||
|
if (_insideAddRenderView
|
||||||
|
&& renderView.hasConfiguration
|
||||||
|
&& renderView.configuration is TestViewConfiguration
|
||||||
|
&& renderView == this.renderView) { // ignore: deprecated_member_use
|
||||||
|
// If a test has reached out to the now deprecated renderView property to set a custom TestViewConfiguration
|
||||||
|
// we are not replacing it. This is to maintain backwards compatibility with how things worked prior to the
|
||||||
|
// deprecation of that property.
|
||||||
|
// TODO(goderbauer): Remove this "if" when the deprecated renderView property is removed.
|
||||||
|
return renderView.configuration;
|
||||||
|
}
|
||||||
|
final FlutterView view = renderView.flutterView;
|
||||||
|
if (_surfaceSize != null && view == platformDispatcher.implicitView) {
|
||||||
return ViewConfiguration(
|
return ViewConfiguration(
|
||||||
size: size,
|
size: _surfaceSize!,
|
||||||
devicePixelRatio: devicePixelRatio,
|
devicePixelRatio: view.devicePixelRatio,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return super.createViewConfigurationFor(renderView);
|
||||||
|
}
|
||||||
|
|
||||||
/// Acts as if the application went idle.
|
/// Acts as if the application went idle.
|
||||||
///
|
///
|
||||||
@ -1377,16 +1403,18 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
|
|||||||
debugBuildingDirtyElements = true;
|
debugBuildingDirtyElements = true;
|
||||||
buildOwner!.buildScope(rootElement!);
|
buildOwner!.buildScope(rootElement!);
|
||||||
if (_phase != EnginePhase.build) {
|
if (_phase != EnginePhase.build) {
|
||||||
pipelineOwner.flushLayout();
|
rootPipelineOwner.flushLayout();
|
||||||
if (_phase != EnginePhase.layout) {
|
if (_phase != EnginePhase.layout) {
|
||||||
pipelineOwner.flushCompositingBits();
|
rootPipelineOwner.flushCompositingBits();
|
||||||
if (_phase != EnginePhase.compositingBits) {
|
if (_phase != EnginePhase.compositingBits) {
|
||||||
pipelineOwner.flushPaint();
|
rootPipelineOwner.flushPaint();
|
||||||
if (_phase != EnginePhase.paint && sendFramesToEngine) {
|
if (_phase != EnginePhase.paint && sendFramesToEngine) {
|
||||||
_firstFrameSent = true;
|
_firstFrameSent = true;
|
||||||
|
for (final RenderView renderView in renderViews) {
|
||||||
renderView.compositeFrame(); // this sends the bits to the GPU
|
renderView.compositeFrame(); // this sends the bits to the GPU
|
||||||
|
}
|
||||||
if (_phase != EnginePhase.composite) {
|
if (_phase != EnginePhase.composite) {
|
||||||
pipelineOwner.flushSemantics();
|
rootPipelineOwner.flushSemantics(); // this sends the semantics to the OS.
|
||||||
assert(_phase == EnginePhase.flushSemantics ||
|
assert(_phase == EnginePhase.flushSemantics ||
|
||||||
_phase == EnginePhase.sendSemanticsUpdate);
|
_phase == EnginePhase.sendSemanticsUpdate);
|
||||||
}
|
}
|
||||||
@ -1759,10 +1787,15 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _markViewNeedsPaint() {
|
void _markViewsNeedPaint([int? viewId]) {
|
||||||
_viewNeedsPaint = true;
|
_viewNeedsPaint = true;
|
||||||
|
final Iterable<RenderView> toMark = viewId == null
|
||||||
|
? renderViews
|
||||||
|
: renderViews.where((RenderView renderView) => renderView.flutterView.viewId == viewId);
|
||||||
|
for (final RenderView renderView in toMark) {
|
||||||
renderView.markNeedsPaint();
|
renderView.markNeedsPaint();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TextPainter? _label;
|
TextPainter? _label;
|
||||||
static const TextStyle _labelStyle = TextStyle(
|
static const TextStyle _labelStyle = TextStyle(
|
||||||
@ -1779,15 +1812,16 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
|
|||||||
_label ??= TextPainter(textAlign: TextAlign.left, textDirection: TextDirection.ltr);
|
_label ??= TextPainter(textAlign: TextAlign.left, textDirection: TextDirection.ltr);
|
||||||
_label!.text = TextSpan(text: value, style: _labelStyle);
|
_label!.text = TextSpan(text: value, style: _labelStyle);
|
||||||
_label!.layout();
|
_label!.layout();
|
||||||
_markViewNeedsPaint();
|
_markViewsNeedPaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
final Map<int, _LiveTestPointerRecord> _pointerIdToPointerRecord = <int, _LiveTestPointerRecord>{};
|
final Expando<Map<int, _LiveTestPointerRecord>> _renderViewToPointerIdToPointerRecord = Expando<Map<int, _LiveTestPointerRecord>>();
|
||||||
|
|
||||||
void _handleRenderViewPaint(PaintingContext context, Offset offset, RenderView renderView) {
|
void _handleRenderViewPaint(PaintingContext context, Offset offset, RenderView renderView) {
|
||||||
assert(offset == Offset.zero);
|
assert(offset == Offset.zero);
|
||||||
|
|
||||||
if (_pointerIdToPointerRecord.isNotEmpty) {
|
final Map<int, _LiveTestPointerRecord>? pointerIdToRecord = _renderViewToPointerIdToPointerRecord[renderView];
|
||||||
|
if (pointerIdToRecord != null && pointerIdToRecord.isNotEmpty) {
|
||||||
final double radius = renderView.configuration.size.shortestSide * 0.05;
|
final double radius = renderView.configuration.size.shortestSide * 0.05;
|
||||||
final Path path = Path()
|
final Path path = Path()
|
||||||
..addOval(Rect.fromCircle(center: Offset.zero, radius: radius))
|
..addOval(Rect.fromCircle(center: Offset.zero, radius: radius))
|
||||||
@ -1800,7 +1834,7 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
|
|||||||
..strokeWidth = radius / 10.0
|
..strokeWidth = radius / 10.0
|
||||||
..style = PaintingStyle.stroke;
|
..style = PaintingStyle.stroke;
|
||||||
bool dirty = false;
|
bool dirty = false;
|
||||||
for (final _LiveTestPointerRecord record in _pointerIdToPointerRecord.values) {
|
for (final _LiveTestPointerRecord record in pointerIdToRecord.values) {
|
||||||
paint.color = record.color.withOpacity(record.decay < 0 ? (record.decay / (_kPointerDecay - 1)) : 1.0);
|
paint.color = record.color.withOpacity(record.decay < 0 ? (record.decay / (_kPointerDecay - 1)) : 1.0);
|
||||||
canvas.drawPath(path.shift(record.position), paint);
|
canvas.drawPath(path.shift(record.position), paint);
|
||||||
if (record.decay < 0) {
|
if (record.decay < 0) {
|
||||||
@ -1808,14 +1842,14 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
|
|||||||
}
|
}
|
||||||
record.decay += 1;
|
record.decay += 1;
|
||||||
}
|
}
|
||||||
_pointerIdToPointerRecord
|
pointerIdToRecord
|
||||||
.keys
|
.keys
|
||||||
.where((int pointer) => _pointerIdToPointerRecord[pointer]!.decay == 0)
|
.where((int pointer) => pointerIdToRecord[pointer]!.decay == 0)
|
||||||
.toList()
|
.toList()
|
||||||
.forEach(_pointerIdToPointerRecord.remove);
|
.forEach(pointerIdToRecord.remove);
|
||||||
if (dirty) {
|
if (dirty) {
|
||||||
scheduleMicrotask(() {
|
scheduleMicrotask(() {
|
||||||
_markViewNeedsPaint();
|
_markViewsNeedPaint(renderView.flutterView.viewId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1846,19 +1880,29 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
|
|||||||
void handlePointerEvent(PointerEvent event) {
|
void handlePointerEvent(PointerEvent event) {
|
||||||
switch (pointerEventSource) {
|
switch (pointerEventSource) {
|
||||||
case TestBindingEventSource.test:
|
case TestBindingEventSource.test:
|
||||||
final _LiveTestPointerRecord? record = _pointerIdToPointerRecord[event.pointer];
|
RenderView? target;
|
||||||
|
for (final RenderView renderView in renderViews) {
|
||||||
|
if (renderView.flutterView.viewId == event.viewId) {
|
||||||
|
target = renderView;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (target != null) {
|
||||||
|
final _LiveTestPointerRecord? record = _renderViewToPointerIdToPointerRecord[target]?[event.pointer];
|
||||||
if (record != null) {
|
if (record != null) {
|
||||||
record.position = event.position;
|
record.position = event.position;
|
||||||
if (!event.down) {
|
if (!event.down) {
|
||||||
record.decay = _kPointerDecay;
|
record.decay = _kPointerDecay;
|
||||||
}
|
}
|
||||||
_markViewNeedsPaint();
|
_markViewsNeedPaint(event.viewId);
|
||||||
} else if (event.down) {
|
} else if (event.down) {
|
||||||
_pointerIdToPointerRecord[event.pointer] = _LiveTestPointerRecord(
|
_renderViewToPointerIdToPointerRecord[target] ??= <int, _LiveTestPointerRecord>{};
|
||||||
|
_renderViewToPointerIdToPointerRecord[target]![event.pointer] = _LiveTestPointerRecord(
|
||||||
event.pointer,
|
event.pointer,
|
||||||
event.position,
|
event.position,
|
||||||
);
|
);
|
||||||
_markViewNeedsPaint();
|
_markViewsNeedPaint(event.viewId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
super.handlePointerEvent(event);
|
super.handlePointerEvent(event);
|
||||||
case TestBindingEventSource.device:
|
case TestBindingEventSource.device:
|
||||||
@ -1870,6 +1914,7 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
|
|||||||
// The pointer events received with this source has a global position
|
// The pointer events received with this source has a global position
|
||||||
// (see [handlePointerEventForSource]). Transform it to the local
|
// (see [handlePointerEventForSource]). Transform it to the local
|
||||||
// coordinate space used by the testing widgets.
|
// coordinate space used by the testing widgets.
|
||||||
|
final RenderView renderView = renderViews.firstWhere((RenderView r) => r.flutterView.viewId == event.viewId);
|
||||||
final PointerEvent localEvent = event.copyWith(position: globalToLocal(event.position, renderView));
|
final PointerEvent localEvent = event.copyWith(position: globalToLocal(event.position, renderView));
|
||||||
withPointerEventSource(TestBindingEventSource.device,
|
withPointerEventSource(TestBindingEventSource.device,
|
||||||
() => super.handlePointerEvent(localEvent)
|
() => super.handlePointerEvent(localEvent)
|
||||||
@ -1987,10 +2032,18 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ViewConfiguration createViewConfiguration() {
|
ViewConfiguration createViewConfigurationFor(RenderView renderView) {
|
||||||
|
final FlutterView view = renderView.flutterView;
|
||||||
|
if (view == platformDispatcher.implicitView) {
|
||||||
return TestViewConfiguration.fromView(
|
return TestViewConfiguration.fromView(
|
||||||
size: _surfaceSize ?? _kDefaultTestViewportSize,
|
size: _surfaceSize ?? _kDefaultTestViewportSize,
|
||||||
view: platformDispatcher.implicitView!,
|
view: view,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final double devicePixelRatio = view.devicePixelRatio;
|
||||||
|
return TestViewConfiguration.fromView(
|
||||||
|
size: view.physicalSize / devicePixelRatio,
|
||||||
|
view: view,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ class SemanticsController {
|
|||||||
/// Creates a [SemanticsController] that uses the given binding. Will be
|
/// Creates a [SemanticsController] that uses the given binding. Will be
|
||||||
/// automatically created as part of instantiating a [WidgetController], but
|
/// automatically created as part of instantiating a [WidgetController], but
|
||||||
/// a custom implementation can be passed via the [WidgetController] constructor.
|
/// a custom implementation can be passed via the [WidgetController] constructor.
|
||||||
SemanticsController._(WidgetsBinding binding) : _binding = binding;
|
SemanticsController._(this._controller);
|
||||||
|
|
||||||
static final int _scrollingActions =
|
static final int _scrollingActions =
|
||||||
SemanticsAction.scrollUp.index |
|
SemanticsAction.scrollUp.index |
|
||||||
@ -55,7 +55,7 @@ class SemanticsController {
|
|||||||
SemanticsFlag.isSlider.index |
|
SemanticsFlag.isSlider.index |
|
||||||
SemanticsFlag.isInMutuallyExclusiveGroup.index;
|
SemanticsFlag.isInMutuallyExclusiveGroup.index;
|
||||||
|
|
||||||
final WidgetsBinding _binding;
|
final WidgetController _controller;
|
||||||
|
|
||||||
/// Attempts to find the [SemanticsNode] of first result from `finder`.
|
/// Attempts to find the [SemanticsNode] of first result from `finder`.
|
||||||
///
|
///
|
||||||
@ -73,7 +73,7 @@ class SemanticsController {
|
|||||||
/// if no semantics are found or are not enabled.
|
/// if no semantics are found or are not enabled.
|
||||||
SemanticsNode find(Finder finder) {
|
SemanticsNode find(Finder finder) {
|
||||||
TestAsyncUtils.guardSync();
|
TestAsyncUtils.guardSync();
|
||||||
if (!_binding.semanticsEnabled) {
|
if (!_controller.binding.semanticsEnabled) {
|
||||||
throw StateError('Semantics are not enabled.');
|
throw StateError('Semantics are not enabled.');
|
||||||
}
|
}
|
||||||
final Iterable<Element> candidates = finder.evaluate();
|
final Iterable<Element> candidates = finder.evaluate();
|
||||||
@ -109,6 +109,13 @@ class SemanticsController {
|
|||||||
/// tree. If `end` finds zero elements or more than one element, a
|
/// tree. If `end` finds zero elements or more than one element, a
|
||||||
/// [StateError] will be thrown.
|
/// [StateError] will be thrown.
|
||||||
///
|
///
|
||||||
|
/// If provided, the nodes for `end` and `start` must be part of the same
|
||||||
|
/// semantics tree, i.e. they must be part of the same view.
|
||||||
|
///
|
||||||
|
/// If neither `start` or `end` is provided, `view` can be provided to specify
|
||||||
|
/// the semantics tree to traverse. If `view` is left unspecified,
|
||||||
|
/// [WidgetTester.view] is traversed by default.
|
||||||
|
///
|
||||||
/// Since the order is simulated, edge cases that differ between platforms
|
/// Since the order is simulated, edge cases that differ between platforms
|
||||||
/// (such as how the last visible item in a scrollable list is handled) may be
|
/// (such as how the last visible item in a scrollable list is handled) may be
|
||||||
/// inconsistent with platform behavior, but are expected to be sufficient for
|
/// inconsistent with platform behavior, but are expected to be sufficient for
|
||||||
@ -139,10 +146,47 @@ class SemanticsController {
|
|||||||
/// parts of the traversal.
|
/// parts of the traversal.
|
||||||
/// * [orderedEquals], which can be given an [Iterable<Matcher>] to exactly
|
/// * [orderedEquals], which can be given an [Iterable<Matcher>] to exactly
|
||||||
/// match the order of the traversal.
|
/// match the order of the traversal.
|
||||||
Iterable<SemanticsNode> simulatedAccessibilityTraversal({Finder? start, Finder? end}) {
|
Iterable<SemanticsNode> simulatedAccessibilityTraversal({Finder? start, Finder? end, FlutterView? view}) {
|
||||||
TestAsyncUtils.guardSync();
|
TestAsyncUtils.guardSync();
|
||||||
|
FlutterView? startView;
|
||||||
|
FlutterView? endView;
|
||||||
|
if (start != null) {
|
||||||
|
startView = _controller.viewOf(start);
|
||||||
|
if (view != null && startView != view) {
|
||||||
|
throw StateError(
|
||||||
|
'The start node is not part of the provided view.\n'
|
||||||
|
'Finder: ${start.description}\n'
|
||||||
|
'View of start node: $startView\n'
|
||||||
|
'Specified view: $view'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (end != null) {
|
||||||
|
endView = _controller.viewOf(end);
|
||||||
|
if (view != null && endView != view) {
|
||||||
|
throw StateError(
|
||||||
|
'The end node is not part of the provided view.\n'
|
||||||
|
'Finder: ${end.description}\n'
|
||||||
|
'View of end node: $endView\n'
|
||||||
|
'Specified view: $view'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (endView != null && startView != null && endView != startView) {
|
||||||
|
throw StateError(
|
||||||
|
'The start and end node are in different views.\n'
|
||||||
|
'Start finder: ${start!.description}\n'
|
||||||
|
'End finder: ${end!.description}\n'
|
||||||
|
'View of start node: $startView\n'
|
||||||
|
'View of end node: $endView'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final FlutterView actualView = view ?? startView ?? endView ?? _controller.view;
|
||||||
|
final RenderView renderView = _controller.binding.renderViews.firstWhere((RenderView r) => r.flutterView == actualView);
|
||||||
|
|
||||||
final List<SemanticsNode> traversal = <SemanticsNode>[];
|
final List<SemanticsNode> traversal = <SemanticsNode>[];
|
||||||
_traverse(_binding.pipelineOwner.semanticsOwner!.rootSemanticsNode!, traversal);
|
_traverse(renderView.owner!.semanticsOwner!.rootSemanticsNode!, traversal);
|
||||||
|
|
||||||
int startIndex = 0;
|
int startIndex = 0;
|
||||||
int endIndex = traversal.length - 1;
|
int endIndex = traversal.length - 1;
|
||||||
@ -229,8 +273,7 @@ class SemanticsController {
|
|||||||
/// Concrete subclasses must implement the [pump] method.
|
/// Concrete subclasses must implement the [pump] method.
|
||||||
abstract class WidgetController {
|
abstract class WidgetController {
|
||||||
/// Creates a widget controller that uses the given binding.
|
/// Creates a widget controller that uses the given binding.
|
||||||
WidgetController(this.binding)
|
WidgetController(this.binding);
|
||||||
: _semantics = SemanticsController._(binding);
|
|
||||||
|
|
||||||
/// A reference to the current instance of the binding.
|
/// A reference to the current instance of the binding.
|
||||||
final WidgetsBinding binding;
|
final WidgetsBinding binding;
|
||||||
@ -280,7 +323,7 @@ abstract class WidgetController {
|
|||||||
|
|
||||||
return _semantics;
|
return _semantics;
|
||||||
}
|
}
|
||||||
final SemanticsController _semantics;
|
late final SemanticsController _semantics = SemanticsController._(this);
|
||||||
|
|
||||||
// FINDER API
|
// FINDER API
|
||||||
|
|
||||||
@ -297,14 +340,16 @@ abstract class WidgetController {
|
|||||||
/// * [view] which returns the [TestFlutterView] used when only a single
|
/// * [view] which returns the [TestFlutterView] used when only a single
|
||||||
/// view is being used.
|
/// view is being used.
|
||||||
TestFlutterView viewOf(Finder finder) {
|
TestFlutterView viewOf(Finder finder) {
|
||||||
final View view = firstWidget<View>(
|
return _viewOf(finder) as TestFlutterView;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlutterView _viewOf(Finder finder) {
|
||||||
|
return firstWidget<View>(
|
||||||
find.ancestor(
|
find.ancestor(
|
||||||
of: finder,
|
of: finder,
|
||||||
matching: find.byType(View),
|
matching: find.byType(View),
|
||||||
)
|
),
|
||||||
);
|
).view;
|
||||||
|
|
||||||
return view.view as TestFlutterView;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if `finder` exists in the tree.
|
/// Checks if `finder` exists in the tree.
|
||||||
@ -516,7 +561,12 @@ abstract class WidgetController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a list of all the [Layer] objects in the rendering.
|
/// Returns a list of all the [Layer] objects in the rendering.
|
||||||
List<Layer> get layers => _walkLayers(binding.renderView.debugLayer!).toList();
|
List<Layer> get layers {
|
||||||
|
return <Layer>[
|
||||||
|
for (final RenderView renderView in binding.renderViews)
|
||||||
|
..._walkLayers(renderView.debugLayer!)
|
||||||
|
];
|
||||||
|
}
|
||||||
Iterable<Layer> _walkLayers(Layer layer) sync* {
|
Iterable<Layer> _walkLayers(Layer layer) sync* {
|
||||||
TestAsyncUtils.guardSync();
|
TestAsyncUtils.guardSync();
|
||||||
yield layer;
|
yield layer;
|
||||||
@ -1190,10 +1240,10 @@ abstract class WidgetController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Forwards the given location to the binding's hitTest logic.
|
/// Forwards the given location to the binding's hitTest logic.
|
||||||
HitTestResult hitTestOnBinding(Offset location) {
|
HitTestResult hitTestOnBinding(Offset location, { int? viewId }) {
|
||||||
|
viewId ??= view.viewId;
|
||||||
final HitTestResult result = HitTestResult();
|
final HitTestResult result = HitTestResult();
|
||||||
// TODO(goderbauer): Support multiple views in flutter_test pointer event handling, https://github.com/flutter/flutter/issues/128281
|
binding.hitTestInView(result, location, viewId);
|
||||||
binding.hitTest(result, location); // ignore: deprecated_member_use
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1313,9 +1363,9 @@ abstract class WidgetController {
|
|||||||
final RenderBox box = element.renderObject! as RenderBox;
|
final RenderBox box = element.renderObject! as RenderBox;
|
||||||
final Offset location = box.localToGlobal(sizeToPoint(box.size));
|
final Offset location = box.localToGlobal(sizeToPoint(box.size));
|
||||||
if (warnIfMissed) {
|
if (warnIfMissed) {
|
||||||
|
final FlutterView view = _viewOf(finder);
|
||||||
final HitTestResult result = HitTestResult();
|
final HitTestResult result = HitTestResult();
|
||||||
// TODO(goderbauer): Support multiple views in flutter_test pointer event handling, https://github.com/flutter/flutter/issues/128281
|
binding.hitTestInView(result, location, view.viewId);
|
||||||
binding.hitTest(result, location); // ignore: deprecated_member_use
|
|
||||||
bool found = false;
|
bool found = false;
|
||||||
for (final HitTestEntry entry in result.path) {
|
for (final HitTestEntry entry in result.path) {
|
||||||
if (entry.target == box) {
|
if (entry.target == box) {
|
||||||
@ -1324,15 +1374,16 @@ abstract class WidgetController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!found) {
|
if (!found) {
|
||||||
|
final RenderView renderView = binding.renderViews.firstWhere((RenderView r) => r.flutterView == view);
|
||||||
bool outOfBounds = false;
|
bool outOfBounds = false;
|
||||||
outOfBounds = !(Offset.zero & binding.renderView.size).contains(location);
|
outOfBounds = !(Offset.zero & renderView.size).contains(location);
|
||||||
if (hitTestWarningShouldBeFatal) {
|
if (hitTestWarningShouldBeFatal) {
|
||||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||||
ErrorSummary('Finder specifies a widget that would not receive pointer events.'),
|
ErrorSummary('Finder specifies a widget that would not receive pointer events.'),
|
||||||
ErrorDescription('A call to $callee() with finder "$finder" derived an Offset ($location) that would not hit test on the specified widget.'),
|
ErrorDescription('A call to $callee() with finder "$finder" derived an Offset ($location) that would not hit test on the specified widget.'),
|
||||||
ErrorHint('Maybe the widget is actually off-screen, or another widget is obscuring it, or the widget cannot receive pointer events.'),
|
ErrorHint('Maybe the widget is actually off-screen, or another widget is obscuring it, or the widget cannot receive pointer events.'),
|
||||||
if (outOfBounds)
|
if (outOfBounds)
|
||||||
ErrorHint('Indeed, $location is outside the bounds of the root of the render tree, ${binding.renderView.size}.'),
|
ErrorHint('Indeed, $location is outside the bounds of the root of the render tree, ${renderView.size}.'),
|
||||||
box.toDiagnosticsNode(name: 'The finder corresponds to this RenderBox', style: DiagnosticsTreeStyle.singleLine),
|
box.toDiagnosticsNode(name: 'The finder corresponds to this RenderBox', style: DiagnosticsTreeStyle.singleLine),
|
||||||
ErrorDescription('The hit test result at that offset is: $result'),
|
ErrorDescription('The hit test result at that offset is: $result'),
|
||||||
ErrorDescription('If you expected this target not to be able to receive pointer events, pass "warnIfMissed: false" to "$callee()".'),
|
ErrorDescription('If you expected this target not to be able to receive pointer events, pass "warnIfMissed: false" to "$callee()".'),
|
||||||
@ -1343,7 +1394,7 @@ abstract class WidgetController {
|
|||||||
'\n'
|
'\n'
|
||||||
'Warning: A call to $callee() with finder "$finder" derived an Offset ($location) that would not hit test on the specified widget.\n'
|
'Warning: A call to $callee() with finder "$finder" derived an Offset ($location) that would not hit test on the specified widget.\n'
|
||||||
'Maybe the widget is actually off-screen, or another widget is obscuring it, or the widget cannot receive pointer events.\n'
|
'Maybe the widget is actually off-screen, or another widget is obscuring it, or the widget cannot receive pointer events.\n'
|
||||||
'${outOfBounds ? "Indeed, $location is outside the bounds of the root of the render tree, ${binding.renderView.size}.\n" : ""}'
|
'${outOfBounds ? "Indeed, $location is outside the bounds of the root of the render tree, ${renderView.size}.\n" : ""}'
|
||||||
'The finder corresponds to this RenderBox: $box\n'
|
'The finder corresponds to this RenderBox: $box\n'
|
||||||
'The hit test result at that offset is: $result\n'
|
'The hit test result at that offset is: $result\n'
|
||||||
'${StackTrace.current}'
|
'${StackTrace.current}'
|
||||||
|
@ -639,11 +639,11 @@ class _HitTestableFinder extends ChainedFinder {
|
|||||||
@override
|
@override
|
||||||
Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
|
Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
|
||||||
for (final Element candidate in parentCandidates) {
|
for (final Element candidate in parentCandidates) {
|
||||||
|
final int viewId = candidate.findAncestorWidgetOfExactType<View>()!.view.viewId;
|
||||||
final RenderBox box = candidate.renderObject! as RenderBox;
|
final RenderBox box = candidate.renderObject! as RenderBox;
|
||||||
final Offset absoluteOffset = box.localToGlobal(alignment.alongSize(box.size));
|
final Offset absoluteOffset = box.localToGlobal(alignment.alongSize(box.size));
|
||||||
final HitTestResult hitResult = HitTestResult();
|
final HitTestResult hitResult = HitTestResult();
|
||||||
// TODO(goderbauer): Support multiple views in flutter_test pointer event handling, https://github.com/flutter/flutter/issues/128281
|
WidgetsBinding.instance.hitTestInView(hitResult, absoluteOffset, viewId);
|
||||||
WidgetsBinding.instance.hitTest(hitResult, absoluteOffset); // ignore: deprecated_member_use
|
|
||||||
for (final HitTestEntry entry in hitResult.path) {
|
for (final HitTestEntry entry in hitResult.path) {
|
||||||
if (entry.target == candidate.renderObject) {
|
if (entry.target == candidate.renderObject) {
|
||||||
yield candidate;
|
yield candidate;
|
||||||
|
@ -569,22 +569,10 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
|
|||||||
EnginePhase phase = EnginePhase.sendSemanticsUpdate,
|
EnginePhase phase = EnginePhase.sendSemanticsUpdate,
|
||||||
]) {
|
]) {
|
||||||
return TestAsyncUtils.guard<void>(() {
|
return TestAsyncUtils.guard<void>(() {
|
||||||
return _pumpWidget(
|
binding.attachRootWidget(binding.wrapWithDefaultView(widget));
|
||||||
binding.wrapWithDefaultView(widget),
|
|
||||||
duration,
|
|
||||||
phase,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _pumpWidget(
|
|
||||||
Widget widget, [
|
|
||||||
Duration? duration,
|
|
||||||
EnginePhase phase = EnginePhase.sendSemanticsUpdate,
|
|
||||||
]) {
|
|
||||||
binding.attachRootWidget(widget);
|
|
||||||
binding.scheduleFrame();
|
binding.scheduleFrame();
|
||||||
return binding.pump(duration, phase);
|
return binding.pump(duration, phase);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -745,12 +733,14 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
|
|||||||
'your widget tree in a RootRestorationScope?',
|
'your widget tree in a RootRestorationScope?',
|
||||||
);
|
);
|
||||||
return TestAsyncUtils.guard<void>(() async {
|
return TestAsyncUtils.guard<void>(() async {
|
||||||
final Widget widget = ((binding.rootElement! as RenderObjectToWidgetElement<RenderObject>).widget as RenderObjectToWidgetAdapter<RenderObject>).child!;
|
final RootWidget widget = binding.rootElement!.widget as RootWidget;
|
||||||
final TestRestorationData restorationData = binding.restorationManager.restorationData;
|
final TestRestorationData restorationData = binding.restorationManager.restorationData;
|
||||||
runApp(Container(key: UniqueKey()));
|
runApp(Container(key: UniqueKey()));
|
||||||
await pump();
|
await pump();
|
||||||
binding.restorationManager.restoreFrom(restorationData);
|
binding.restorationManager.restoreFrom(restorationData);
|
||||||
return _pumpWidget(widget);
|
binding.attachToBuildOwner(widget);
|
||||||
|
binding.scheduleFrame();
|
||||||
|
return binding.pump();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -837,9 +827,11 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
|
|||||||
bool get hasRunningAnimations => binding.transientCallbackCount > 0;
|
bool get hasRunningAnimations => binding.transientCallbackCount > 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
HitTestResult hitTestOnBinding(Offset location) {
|
HitTestResult hitTestOnBinding(Offset location, {int? viewId}) {
|
||||||
location = binding.localToGlobal(location, binding.renderView);
|
viewId ??= view.viewId;
|
||||||
return super.hitTestOnBinding(location);
|
final RenderView renderView = binding.renderViews.firstWhere((RenderView r) => r.flutterView.viewId == viewId);
|
||||||
|
location = binding.localToGlobal(location, renderView);
|
||||||
|
return super.hitTestOnBinding(location, viewId: viewId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -861,7 +853,9 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
|
|||||||
.map((HitTestEntry candidate) => candidate.target)
|
.map((HitTestEntry candidate) => candidate.target)
|
||||||
.whereType<RenderObject>()
|
.whereType<RenderObject>()
|
||||||
.first;
|
.first;
|
||||||
final Element? innerTargetElement = _lastWhereOrNull(
|
final Element? innerTargetElement = binding.renderViews.contains(innerTarget)
|
||||||
|
? null
|
||||||
|
: _lastWhereOrNull(
|
||||||
collectAllElementsFrom(binding.rootElement!, skipOffstage: true),
|
collectAllElementsFrom(binding.rootElement!, skipOffstage: true),
|
||||||
(Element element) => element.renderObject == innerTarget,
|
(Element element) => element.renderObject == innerTarget,
|
||||||
);
|
);
|
||||||
@ -1060,6 +1054,8 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
|
|||||||
|
|
||||||
int? _lastRecordedSemanticsHandles;
|
int? _lastRecordedSemanticsHandles;
|
||||||
|
|
||||||
|
// TODO(goderbauer): Only use binding.debugOutstandingSemanticsHandles when deprecated binding.pipelineOwner is removed.
|
||||||
|
// ignore: deprecated_member_use
|
||||||
int get _currentSemanticsHandles => binding.debugOutstandingSemanticsHandles + binding.pipelineOwner.debugOutstandingSemanticsHandles;
|
int get _currentSemanticsHandles => binding.debugOutstandingSemanticsHandles + binding.pipelineOwner.debugOutstandingSemanticsHandles;
|
||||||
|
|
||||||
void _recordNumberOfSemanticsHandles() {
|
void _recordNumberOfSemanticsHandles() {
|
||||||
|
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() {
|
void pipelineOwnerTestRun() {
|
||||||
testWidgets('open SemanticsHandle from PipelineOwner fails test', (WidgetTester tester) async {
|
testWidgets('open SemanticsHandle from PipelineOwner fails test', (WidgetTester tester) async {
|
||||||
final int outstandingHandles = tester.binding.pipelineOwner.debugOutstandingSemanticsHandles;
|
final int outstandingHandles = tester.binding.debugOutstandingSemanticsHandles;
|
||||||
tester.binding.pipelineOwner.ensureSemantics();
|
tester.binding.ensureSemantics();
|
||||||
expect(tester.binding.pipelineOwner.debugOutstandingSemanticsHandles, outstandingHandles + 1);
|
expect(tester.binding.debugOutstandingSemanticsHandles, outstandingHandles + 1);
|
||||||
// SemanticsHandle is not disposed on purpose to verify in tearDown that
|
// SemanticsHandle is not disposed on purpose to verify in tearDown that
|
||||||
// the test failed due to an active SemanticsHandle.
|
// the test failed due to an active SemanticsHandle.
|
||||||
});
|
});
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
// Only check the initial lines of the message, since the message walks the
|
// Only check the initial lines of the message, since the message walks the
|
||||||
@ -82,7 +83,7 @@ No widgets found at Offset(1.0, 1.0).
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final Size originalSize = tester.binding.createViewConfiguration().size;
|
final Size originalSize = tester.binding.createViewConfigurationFor(tester.binding.renderView).size; // ignore: deprecated_member_use
|
||||||
await tester.binding.setSurfaceSize(const Size(2000, 1800));
|
await tester.binding.setSurfaceSize(const Size(2000, 1800));
|
||||||
try {
|
try {
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
@ -126,6 +127,7 @@ class _MockLiveTestWidgetsFlutterBinding extends LiveTestWidgetsFlutterBinding {
|
|||||||
// real devices touches sends event in the global coordinate system.
|
// real devices touches sends event in the global coordinate system.
|
||||||
// See the documentation of [handlePointerEventForSource] for details.
|
// See the documentation of [handlePointerEventForSource] for details.
|
||||||
if (source == TestBindingEventSource.test) {
|
if (source == TestBindingEventSource.test) {
|
||||||
|
final RenderView renderView = renderViews.firstWhere((RenderView r) => r.flutterView.viewId == event.viewId);
|
||||||
final PointerEvent globalEvent = event.copyWith(position: localToGlobal(event.position, renderView));
|
final PointerEvent globalEvent = event.copyWith(position: localToGlobal(event.position, renderView));
|
||||||
return super.handlePointerEventForSource(globalEvent);
|
return super.handlePointerEventForSource(globalEvent);
|
||||||
}
|
}
|
||||||
|
@ -100,10 +100,6 @@ https://flutter.dev/docs/testing/integration-tests#testing-on-firebase-test-lab
|
|||||||
// under debug mode.
|
// under debug mode.
|
||||||
static bool _firstRun = false;
|
static bool _firstRun = false;
|
||||||
|
|
||||||
/// Artificially changes the surface size to `size` on the Widget binding,
|
|
||||||
/// then flushes microtasks.
|
|
||||||
///
|
|
||||||
/// Set to null to use the default surface size.
|
|
||||||
@override
|
@override
|
||||||
Future<void> setSurfaceSize(Size? size) {
|
Future<void> setSurfaceSize(Size? size) {
|
||||||
return TestAsyncUtils.guard<void>(() async {
|
return TestAsyncUtils.guard<void>(() async {
|
||||||
@ -117,12 +113,11 @@ https://flutter.dev/docs/testing/integration-tests#testing-on-firebase-test-lab
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ViewConfiguration createViewConfiguration() {
|
ViewConfiguration createViewConfigurationFor(RenderView renderView) {
|
||||||
final FlutterView view = platformDispatcher.implicitView!;
|
final FlutterView view = renderView.flutterView;
|
||||||
final double devicePixelRatio = view.devicePixelRatio;
|
final Size? surfaceSize = view == platformDispatcher.implicitView ? _surfaceSize : null;
|
||||||
final Size size = _surfaceSize ?? view.physicalSize / devicePixelRatio;
|
|
||||||
return TestViewConfiguration.fromView(
|
return TestViewConfiguration.fromView(
|
||||||
size: size,
|
size: surfaceSize ?? view.physicalSize / view.devicePixelRatio,
|
||||||
view: view,
|
view: view,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -442,11 +437,11 @@ https://flutter.dev/docs/testing/integration-tests#testing-on-firebase-test-lab
|
|||||||
Timeout defaultTestTimeout = Timeout.none;
|
Timeout defaultTestTimeout = Timeout.none;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void attachRootWidget(Widget rootWidget) {
|
Widget wrapWithDefaultView(Widget rootWidget) {
|
||||||
// This is a workaround where screenshots of root widgets have incorrect
|
// This is a workaround where screenshots of root widgets have incorrect
|
||||||
// bounds.
|
// bounds.
|
||||||
// TODO(jiahaog): Remove when https://github.com/flutter/flutter/issues/66006 is fixed.
|
// TODO(jiahaog): Remove when https://github.com/flutter/flutter/issues/66006 is fixed.
|
||||||
super.attachRootWidget(RepaintBoundary(child: rootWidget));
|
return super.wrapWithDefaultView(RepaintBoundary(child: rootWidget));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
Loading…
Reference in New Issue
Block a user