Merge pull request #3010 from krisgiesing/offscreen_layout
Part 2 of independent layout pipelines
This commit is contained in:
commit
243960d741
@ -33,6 +33,7 @@ class Rectangle extends StatelessWidget {
|
|||||||
|
|
||||||
double value;
|
double value;
|
||||||
RenderObjectToWidgetElement<RenderBox> element;
|
RenderObjectToWidgetElement<RenderBox> element;
|
||||||
|
BuildOwner owner;
|
||||||
void attachWidgetTreeToRenderTree(RenderProxyBox container) {
|
void attachWidgetTreeToRenderTree(RenderProxyBox container) {
|
||||||
element = new RenderObjectToWidgetAdapter<RenderBox>(
|
element = new RenderObjectToWidgetAdapter<RenderBox>(
|
||||||
container: container,
|
container: container,
|
||||||
@ -70,7 +71,7 @@ void attachWidgetTreeToRenderTree(RenderProxyBox container) {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween
|
mainAxisAlignment: MainAxisAlignment.spaceBetween
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
).attachToRenderTree(element);
|
).attachToRenderTree(owner, element);
|
||||||
}
|
}
|
||||||
|
|
||||||
Duration timeBase;
|
Duration timeBase;
|
||||||
|
@ -34,7 +34,7 @@ void main() {
|
|||||||
|
|
||||||
for (int i = 0; i < _kNumberOfIterations || _kRunForever; ++i) {
|
for (int i = 0; i < _kNumberOfIterations || _kRunForever; ++i) {
|
||||||
appState.setState(_doNothing);
|
appState.setState(_doNothing);
|
||||||
binding.buildDirtyElements();
|
binding.buildOwner.buildDirtyElements();
|
||||||
}
|
}
|
||||||
|
|
||||||
watch.stop();
|
watch.stop();
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// 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:developer';
|
|
||||||
import 'dart:ui' as ui show window;
|
import 'dart:ui' as ui show window;
|
||||||
import 'dart:ui' show AppLifecycleState, Locale;
|
import 'dart:ui' show AppLifecycleState, Locale;
|
||||||
|
|
||||||
@ -26,6 +25,10 @@ class BindingObserver {
|
|||||||
/// This is the glue that binds the framework to the Flutter engine.
|
/// This is the glue that binds the framework to the Flutter engine.
|
||||||
class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, Services, Renderer {
|
class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, Services, Renderer {
|
||||||
|
|
||||||
|
WidgetFlutterBinding() {
|
||||||
|
buildOwner.onBuildScheduled = ensureVisualUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates and initializes the WidgetFlutterBinding. This constructor is
|
/// Creates and initializes the WidgetFlutterBinding. This constructor is
|
||||||
/// idempotent; calling it a second time will just return the
|
/// idempotent; calling it a second time will just return the
|
||||||
/// previously-created instance.
|
/// previously-created instance.
|
||||||
@ -35,11 +38,15 @@ class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, Service
|
|||||||
return _instance;
|
return _instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final BuildOwner _buildOwner = new BuildOwner();
|
||||||
|
/// The [BuildOwner] in charge of executing the build pipeline for the
|
||||||
|
/// widget tree rooted at this binding.
|
||||||
|
BuildOwner get buildOwner => _buildOwner;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initInstances() {
|
void initInstances() {
|
||||||
super.initInstances();
|
super.initInstances();
|
||||||
_instance = this;
|
_instance = this;
|
||||||
BuildableElement.scheduleBuildFor = scheduleBuildFor;
|
|
||||||
ui.window.onLocaleChanged = handleLocaleChanged;
|
ui.window.onLocaleChanged = handleLocaleChanged;
|
||||||
ui.window.onPopRoute = handlePopRoute;
|
ui.window.onPopRoute = handlePopRoute;
|
||||||
ui.window.onAppLifecycleStateChanged = handleAppLifecycleStateChanged;
|
ui.window.onAppLifecycleStateChanged = handleAppLifecycleStateChanged;
|
||||||
@ -92,60 +99,9 @@ class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, Service
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void beginFrame() {
|
void beginFrame() {
|
||||||
buildDirtyElements();
|
buildOwner.buildDirtyElements();
|
||||||
super.beginFrame();
|
super.beginFrame();
|
||||||
Element.finalizeTree();
|
buildOwner.finalizeTree();
|
||||||
}
|
|
||||||
|
|
||||||
List<BuildableElement> _dirtyElements = <BuildableElement>[];
|
|
||||||
|
|
||||||
/// Adds an element to the dirty elements list so that it will be rebuilt
|
|
||||||
/// when buildDirtyElements is called.
|
|
||||||
void scheduleBuildFor(BuildableElement element) {
|
|
||||||
assert(!_dirtyElements.contains(element));
|
|
||||||
assert(element.dirty);
|
|
||||||
if (_dirtyElements.isEmpty)
|
|
||||||
ensureVisualUpdate();
|
|
||||||
_dirtyElements.add(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int _elementSort(BuildableElement a, BuildableElement b) {
|
|
||||||
if (a.depth < b.depth)
|
|
||||||
return -1;
|
|
||||||
if (b.depth < a.depth)
|
|
||||||
return 1;
|
|
||||||
if (b.dirty && !a.dirty)
|
|
||||||
return -1;
|
|
||||||
if (a.dirty && !b.dirty)
|
|
||||||
return 1;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds all the elements that were marked as dirty using schedule(), in depth order.
|
|
||||||
/// If elements are marked as dirty while this runs, they must be deeper than the algorithm
|
|
||||||
/// has yet reached.
|
|
||||||
/// This is called by beginFrame().
|
|
||||||
void buildDirtyElements() {
|
|
||||||
if (_dirtyElements.isEmpty)
|
|
||||||
return;
|
|
||||||
Timeline.startSync('Build');
|
|
||||||
BuildableElement.lockState(() {
|
|
||||||
_dirtyElements.sort(_elementSort);
|
|
||||||
int dirtyCount = _dirtyElements.length;
|
|
||||||
int index = 0;
|
|
||||||
while (index < dirtyCount) {
|
|
||||||
_dirtyElements[index].rebuild();
|
|
||||||
index += 1;
|
|
||||||
if (dirtyCount < _dirtyElements.length) {
|
|
||||||
_dirtyElements.sort(_elementSort);
|
|
||||||
dirtyCount = _dirtyElements.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert(!_dirtyElements.any((BuildableElement element) => element.dirty));
|
|
||||||
_dirtyElements.clear();
|
|
||||||
}, building: true);
|
|
||||||
assert(_dirtyElements.isEmpty);
|
|
||||||
Timeline.finishSync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The [Element] that is at the root of the hierarchy (and which wraps the
|
/// The [Element] that is at the root of the hierarchy (and which wraps the
|
||||||
@ -157,7 +113,7 @@ class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, Service
|
|||||||
container: renderView,
|
container: renderView,
|
||||||
debugShortDescription: '[root]',
|
debugShortDescription: '[root]',
|
||||||
child: app
|
child: app
|
||||||
).attachToRenderTree(_renderViewElement);
|
).attachToRenderTree(buildOwner, _renderViewElement);
|
||||||
beginFrame();
|
beginFrame();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -205,10 +161,11 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi
|
|||||||
@override
|
@override
|
||||||
void updateRenderObject(BuildContext context, RenderObject renderObject) { }
|
void updateRenderObject(BuildContext context, RenderObject renderObject) { }
|
||||||
|
|
||||||
RenderObjectToWidgetElement<T> attachToRenderTree([RenderObjectToWidgetElement<T> element]) {
|
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
|
||||||
BuildableElement.lockState(() {
|
owner.lockState(() {
|
||||||
if (element == null) {
|
if (element == null) {
|
||||||
element = createElement();
|
element = createElement();
|
||||||
|
element.assignOwner(owner);
|
||||||
element.mount(null, null);
|
element.mount(null, null);
|
||||||
} else {
|
} else {
|
||||||
element.update(this);
|
element.update(this);
|
||||||
@ -229,7 +186,7 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi
|
|||||||
/// whose container is the RenderView that connects to the Flutter engine. In
|
/// whose container is the RenderView that connects to the Flutter engine. In
|
||||||
/// this usage, it is normally instantiated by the bootstrapping logic in the
|
/// this usage, it is normally instantiated by the bootstrapping logic in the
|
||||||
/// WidgetFlutterBinding singleton created by runApp().
|
/// WidgetFlutterBinding singleton created by runApp().
|
||||||
class RenderObjectToWidgetElement<T extends RenderObject> extends RenderObjectElement {
|
class RenderObjectToWidgetElement<T extends RenderObject> extends RootRenderObjectElement {
|
||||||
RenderObjectToWidgetElement(RenderObjectToWidgetAdapter<T> widget) : super(widget);
|
RenderObjectToWidgetElement(RenderObjectToWidgetAdapter<T> widget) : super(widget);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'debug.dart';
|
import 'debug.dart';
|
||||||
|
|
||||||
@ -613,17 +614,15 @@ class _InactiveElements {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void unmountAll() {
|
void _unmountAll() {
|
||||||
BuildableElement.lockState(() {
|
try {
|
||||||
try {
|
_locked = true;
|
||||||
_locked = true;
|
for (Element element in _elements)
|
||||||
for (Element element in _elements)
|
_unmount(element);
|
||||||
_unmount(element);
|
} finally {
|
||||||
} finally {
|
_elements.clear();
|
||||||
_elements.clear();
|
_locked = false;
|
||||||
_locked = false;
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _deactivateRecursively(Element element) {
|
void _deactivateRecursively(Element element) {
|
||||||
@ -652,8 +651,6 @@ class _InactiveElements {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final _InactiveElements _inactiveElements = new _InactiveElements();
|
|
||||||
|
|
||||||
typedef void ElementVisitor(Element element);
|
typedef void ElementVisitor(Element element);
|
||||||
|
|
||||||
abstract class BuildContext {
|
abstract class BuildContext {
|
||||||
@ -667,6 +664,120 @@ abstract class BuildContext {
|
|||||||
void visitChildElements(void visitor(Element element));
|
void visitChildElements(void visitor(Element element));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BuildOwner {
|
||||||
|
BuildOwner({ this.onBuildScheduled });
|
||||||
|
|
||||||
|
/// Called on each build pass when the first buildable element is marked dirty
|
||||||
|
VoidCallback onBuildScheduled;
|
||||||
|
|
||||||
|
final _InactiveElements _inactiveElements = new _InactiveElements();
|
||||||
|
|
||||||
|
final List<BuildableElement> _dirtyElements = <BuildableElement>[];
|
||||||
|
|
||||||
|
/// Adds an element to the dirty elements list so that it will be rebuilt
|
||||||
|
/// when buildDirtyElements is called.
|
||||||
|
void scheduleBuildFor(BuildableElement element) {
|
||||||
|
assert(!_dirtyElements.contains(element));
|
||||||
|
assert(element.dirty);
|
||||||
|
if (_dirtyElements.isEmpty && onBuildScheduled != null)
|
||||||
|
onBuildScheduled();
|
||||||
|
_dirtyElements.add(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
int _debugStateLockLevel = 0;
|
||||||
|
bool get _debugStateLocked => _debugStateLockLevel > 0;
|
||||||
|
bool _debugBuilding = false;
|
||||||
|
BuildableElement _debugCurrentBuildTarget;
|
||||||
|
|
||||||
|
/// Establishes a scope in which widget build functions can run.
|
||||||
|
///
|
||||||
|
/// Inside a build scope, widget build functions are allowed to run, but
|
||||||
|
/// State.setState() is forbidden. This mechanism prevents build functions
|
||||||
|
/// from transitively requiring other build functions to run, potentially
|
||||||
|
/// causing infinite loops.
|
||||||
|
///
|
||||||
|
/// After unwinding the last build scope on the stack, the framework verifies
|
||||||
|
/// that each global key is used at most once and notifies listeners about
|
||||||
|
/// changes to global keys.
|
||||||
|
void lockState(void callback(), { bool building: false }) {
|
||||||
|
assert(_debugStateLockLevel >= 0);
|
||||||
|
assert(() {
|
||||||
|
if (building) {
|
||||||
|
assert(!_debugBuilding);
|
||||||
|
assert(_debugCurrentBuildTarget == null);
|
||||||
|
_debugBuilding = true;
|
||||||
|
}
|
||||||
|
_debugStateLockLevel += 1;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
callback();
|
||||||
|
} finally {
|
||||||
|
assert(() {
|
||||||
|
_debugStateLockLevel -= 1;
|
||||||
|
if (building) {
|
||||||
|
assert(_debugBuilding);
|
||||||
|
assert(_debugCurrentBuildTarget == null);
|
||||||
|
_debugBuilding = false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
assert(_debugStateLockLevel >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _elementSort(BuildableElement a, BuildableElement b) {
|
||||||
|
if (a.depth < b.depth)
|
||||||
|
return -1;
|
||||||
|
if (b.depth < a.depth)
|
||||||
|
return 1;
|
||||||
|
if (b.dirty && !a.dirty)
|
||||||
|
return -1;
|
||||||
|
if (a.dirty && !b.dirty)
|
||||||
|
return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds all the elements that were marked as dirty using schedule(), in depth order.
|
||||||
|
/// If elements are marked as dirty while this runs, they must be deeper than the algorithm
|
||||||
|
/// has yet reached.
|
||||||
|
/// This is called by beginFrame().
|
||||||
|
void buildDirtyElements() {
|
||||||
|
if (_dirtyElements.isEmpty)
|
||||||
|
return;
|
||||||
|
Timeline.startSync('Build');
|
||||||
|
lockState(() {
|
||||||
|
_dirtyElements.sort(_elementSort);
|
||||||
|
int dirtyCount = _dirtyElements.length;
|
||||||
|
int index = 0;
|
||||||
|
while (index < dirtyCount) {
|
||||||
|
_dirtyElements[index].rebuild();
|
||||||
|
index += 1;
|
||||||
|
if (dirtyCount < _dirtyElements.length) {
|
||||||
|
_dirtyElements.sort(_elementSort);
|
||||||
|
dirtyCount = _dirtyElements.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert(!_dirtyElements.any((BuildableElement element) => element.dirty));
|
||||||
|
_dirtyElements.clear();
|
||||||
|
}, building: true);
|
||||||
|
assert(_dirtyElements.isEmpty);
|
||||||
|
Timeline.finishSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete the element build pass by unmounting any elements that are no
|
||||||
|
/// longer active.
|
||||||
|
/// This is called by beginFrame().
|
||||||
|
void finalizeTree() {
|
||||||
|
lockState(() {
|
||||||
|
_inactiveElements._unmountAll();
|
||||||
|
});
|
||||||
|
assert(GlobalKey._debugCheckForDuplicates);
|
||||||
|
scheduleMicrotask(GlobalKey._notifyListeners);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/// Elements are the instantiations of Widget configurations.
|
/// Elements are the instantiations of Widget configurations.
|
||||||
///
|
///
|
||||||
/// Elements can, in principle, have children. Only subclasses of
|
/// Elements can, in principle, have children. Only subclasses of
|
||||||
@ -696,6 +807,10 @@ abstract class Element implements BuildContext {
|
|||||||
Widget get widget => _widget;
|
Widget get widget => _widget;
|
||||||
Widget _widget;
|
Widget _widget;
|
||||||
|
|
||||||
|
BuildOwner _owner;
|
||||||
|
/// The owner for this node (null if unattached).
|
||||||
|
BuildOwner get owner => _owner;
|
||||||
|
|
||||||
bool _active = false;
|
bool _active = false;
|
||||||
|
|
||||||
RenderObject get renderObject {
|
RenderObject get renderObject {
|
||||||
@ -721,7 +836,7 @@ abstract class Element implements BuildContext {
|
|||||||
@override
|
@override
|
||||||
void visitChildElements(void visitor(Element element)) {
|
void visitChildElements(void visitor(Element element)) {
|
||||||
// don't allow visitChildElements() during build, since children aren't necessarily built yet
|
// don't allow visitChildElements() during build, since children aren't necessarily built yet
|
||||||
assert(!BuildableElement._debugStateLocked);
|
assert(owner == null || !owner._debugStateLocked);
|
||||||
visitChildren(visitor);
|
visitChildren(visitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -774,12 +889,6 @@ abstract class Element implements BuildContext {
|
|||||||
return inflateWidget(newWidget, newSlot);
|
return inflateWidget(newWidget, newSlot);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void finalizeTree() {
|
|
||||||
_inactiveElements.unmountAll();
|
|
||||||
assert(GlobalKey._debugCheckForDuplicates);
|
|
||||||
scheduleMicrotask(GlobalKey._notifyListeners);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Called when an Element is given a new parent shortly after having been
|
/// Called when an Element is given a new parent shortly after having been
|
||||||
/// created. Use this to initialize state that depends on having a parent. For
|
/// created. Use this to initialize state that depends on having a parent. For
|
||||||
/// state that is independent of the position in the tree, it's better to just
|
/// state that is independent of the position in the tree, it's better to just
|
||||||
@ -796,6 +905,8 @@ abstract class Element implements BuildContext {
|
|||||||
_slot = newSlot;
|
_slot = newSlot;
|
||||||
_depth = _parent != null ? _parent.depth + 1 : 1;
|
_depth = _parent != null ? _parent.depth + 1 : 1;
|
||||||
_active = true;
|
_active = true;
|
||||||
|
if (parent != null) // Only assign ownership if the parent is non-null
|
||||||
|
_owner = parent.owner;
|
||||||
if (widget.key is GlobalKey) {
|
if (widget.key is GlobalKey) {
|
||||||
final GlobalKey key = widget.key;
|
final GlobalKey key = widget.key;
|
||||||
key._register(this);
|
key._register(this);
|
||||||
@ -874,7 +985,7 @@ abstract class Element implements BuildContext {
|
|||||||
if (element._parent != null && !element._parent.detachChild(element))
|
if (element._parent != null && !element._parent.detachChild(element))
|
||||||
return null;
|
return null;
|
||||||
assert(element._parent == null);
|
assert(element._parent == null);
|
||||||
_inactiveElements.remove(element);
|
owner._inactiveElements.remove(element);
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -914,7 +1025,7 @@ abstract class Element implements BuildContext {
|
|||||||
assert(child._parent == this);
|
assert(child._parent == this);
|
||||||
child._parent = null;
|
child._parent = null;
|
||||||
child.detachRenderObject();
|
child.detachRenderObject();
|
||||||
_inactiveElements.add(child); // this eventually calls child.deactivate()
|
owner._inactiveElements.add(child); // this eventually calls child.deactivate()
|
||||||
}
|
}
|
||||||
|
|
||||||
void _activateWithParent(Element parent, dynamic newSlot) {
|
void _activateWithParent(Element parent, dynamic newSlot) {
|
||||||
@ -1117,8 +1228,6 @@ class ErrorWidget extends LeafRenderObjectWidget {
|
|||||||
RenderBox createRenderObject(BuildContext context) => new RenderErrorBox(message);
|
RenderBox createRenderObject(BuildContext context) => new RenderErrorBox(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef void BuildScheduler(BuildableElement element);
|
|
||||||
|
|
||||||
/// Base class for instantiations of widgets that have builders and can be
|
/// Base class for instantiations of widgets that have builders and can be
|
||||||
/// marked dirty.
|
/// marked dirty.
|
||||||
abstract class BuildableElement extends Element {
|
abstract class BuildableElement extends Element {
|
||||||
@ -1139,50 +1248,6 @@ abstract class BuildableElement extends Element {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static BuildScheduler scheduleBuildFor;
|
|
||||||
|
|
||||||
static int _debugStateLockLevel = 0;
|
|
||||||
static bool get _debugStateLocked => _debugStateLockLevel > 0;
|
|
||||||
static bool _debugBuilding = false;
|
|
||||||
static BuildableElement _debugCurrentBuildTarget;
|
|
||||||
|
|
||||||
/// Establishes a scope in which widget build functions can run.
|
|
||||||
///
|
|
||||||
/// Inside a build scope, widget build functions are allowed to run, but
|
|
||||||
/// State.setState() is forbidden. This mechanism prevents build functions
|
|
||||||
/// from transitively requiring other build functions to run, potentially
|
|
||||||
/// causing infinite loops.
|
|
||||||
///
|
|
||||||
/// After unwinding the last build scope on the stack, the framework verifies
|
|
||||||
/// that each global key is used at most once and notifies listeners about
|
|
||||||
/// changes to global keys.
|
|
||||||
static void lockState(void callback(), { bool building: false }) {
|
|
||||||
assert(_debugStateLockLevel >= 0);
|
|
||||||
assert(() {
|
|
||||||
if (building) {
|
|
||||||
assert(!_debugBuilding);
|
|
||||||
assert(_debugCurrentBuildTarget == null);
|
|
||||||
_debugBuilding = true;
|
|
||||||
}
|
|
||||||
_debugStateLockLevel += 1;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
callback();
|
|
||||||
} finally {
|
|
||||||
assert(() {
|
|
||||||
_debugStateLockLevel -= 1;
|
|
||||||
if (building) {
|
|
||||||
assert(_debugBuilding);
|
|
||||||
assert(_debugCurrentBuildTarget == null);
|
|
||||||
_debugBuilding = false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
assert(_debugStateLockLevel >= 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Marks the element as dirty and adds it to the global list of widgets to
|
/// Marks the element as dirty and adds it to the global list of widgets to
|
||||||
/// rebuild in the next frame.
|
/// rebuild in the next frame.
|
||||||
///
|
///
|
||||||
@ -1194,10 +1259,11 @@ abstract class BuildableElement extends Element {
|
|||||||
assert(_debugLifecycleState != _ElementLifecycle.defunct);
|
assert(_debugLifecycleState != _ElementLifecycle.defunct);
|
||||||
if (!_active)
|
if (!_active)
|
||||||
return;
|
return;
|
||||||
|
assert(owner != null);
|
||||||
assert(_debugLifecycleState == _ElementLifecycle.active);
|
assert(_debugLifecycleState == _ElementLifecycle.active);
|
||||||
assert(() {
|
assert(() {
|
||||||
if (_debugBuilding) {
|
if (owner._debugBuilding) {
|
||||||
if (_debugCurrentBuildTarget == null) {
|
if (owner._debugCurrentBuildTarget == null) {
|
||||||
// If _debugCurrentBuildTarget is null, we're not actually building a
|
// If _debugCurrentBuildTarget is null, we're not actually building a
|
||||||
// widget but instead building the root of the tree via runApp.
|
// widget but instead building the root of the tree via runApp.
|
||||||
// TODO(abarth): Remove these cases and ensure that we always have
|
// TODO(abarth): Remove these cases and ensure that we always have
|
||||||
@ -1206,7 +1272,7 @@ abstract class BuildableElement extends Element {
|
|||||||
}
|
}
|
||||||
bool foundTarget = false;
|
bool foundTarget = false;
|
||||||
visitAncestorElements((Element element) {
|
visitAncestorElements((Element element) {
|
||||||
if (element == _debugCurrentBuildTarget) {
|
if (element == owner._debugCurrentBuildTarget) {
|
||||||
foundTarget = true;
|
foundTarget = true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -1215,7 +1281,7 @@ abstract class BuildableElement extends Element {
|
|||||||
if (foundTarget)
|
if (foundTarget)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (_debugStateLocked && (!_debugAllowIgnoredCallsToMarkNeedsBuild || !dirty)) {
|
if (owner._debugStateLocked && (!_debugAllowIgnoredCallsToMarkNeedsBuild || !dirty)) {
|
||||||
throw new FlutterError(
|
throw new FlutterError(
|
||||||
'setState() or markNeedsBuild() called during build.\n'
|
'setState() or markNeedsBuild() called during build.\n'
|
||||||
'This widget cannot be marked as needing to build because the framework '
|
'This widget cannot be marked as needing to build because the framework '
|
||||||
@ -1232,8 +1298,7 @@ abstract class BuildableElement extends Element {
|
|||||||
if (dirty)
|
if (dirty)
|
||||||
return;
|
return;
|
||||||
_dirty = true;
|
_dirty = true;
|
||||||
assert(scheduleBuildFor != null);
|
owner.scheduleBuildFor(this);
|
||||||
scheduleBuildFor(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called by the binding when scheduleBuild() has been called to mark this
|
/// Called by the binding when scheduleBuild() has been called to mark this
|
||||||
@ -1246,17 +1311,17 @@ abstract class BuildableElement extends Element {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
assert(_debugLifecycleState == _ElementLifecycle.active);
|
assert(_debugLifecycleState == _ElementLifecycle.active);
|
||||||
assert(_debugStateLocked);
|
assert(owner._debugStateLocked);
|
||||||
BuildableElement debugPreviousBuildTarget;
|
BuildableElement debugPreviousBuildTarget;
|
||||||
assert(() {
|
assert(() {
|
||||||
debugPreviousBuildTarget = _debugCurrentBuildTarget;
|
debugPreviousBuildTarget = owner._debugCurrentBuildTarget;
|
||||||
_debugCurrentBuildTarget = this;
|
owner._debugCurrentBuildTarget = this;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
performRebuild();
|
performRebuild();
|
||||||
assert(() {
|
assert(() {
|
||||||
assert(_debugCurrentBuildTarget == this);
|
assert(owner._debugCurrentBuildTarget == this);
|
||||||
_debugCurrentBuildTarget = debugPreviousBuildTarget;
|
owner._debugCurrentBuildTarget = debugPreviousBuildTarget;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
assert(!_dirty);
|
assert(!_dirty);
|
||||||
@ -1879,6 +1944,26 @@ abstract class RenderObjectElement extends BuildableElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Instantiation of RenderObjectWidgets at the root of the tree
|
||||||
|
///
|
||||||
|
/// Only root elements may have their owner set explicitly. All other
|
||||||
|
/// elements inherit their owner from their parent.
|
||||||
|
abstract class RootRenderObjectElement extends RenderObjectElement {
|
||||||
|
RootRenderObjectElement(RenderObjectWidget widget): super(widget);
|
||||||
|
|
||||||
|
void assignOwner(BuildOwner owner) {
|
||||||
|
_owner = owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void mount(Element parent, dynamic newSlot) {
|
||||||
|
// Root elements should never have parents
|
||||||
|
assert(parent == null);
|
||||||
|
assert(newSlot == null);
|
||||||
|
super.mount(parent, newSlot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Instantiation of RenderObjectWidgets that have no children
|
/// Instantiation of RenderObjectWidgets that have no children
|
||||||
class LeafRenderObjectElement extends RenderObjectElement {
|
class LeafRenderObjectElement extends RenderObjectElement {
|
||||||
LeafRenderObjectElement(LeafRenderObjectWidget widget): super(widget);
|
LeafRenderObjectElement(LeafRenderObjectWidget widget): super(widget);
|
||||||
|
@ -252,7 +252,7 @@ class _MixedViewportElement extends RenderObjectElement {
|
|||||||
_resetCache();
|
_resetCache();
|
||||||
_lastLayoutConstraints = constraints;
|
_lastLayoutConstraints = constraints;
|
||||||
}
|
}
|
||||||
BuildableElement.lockState(() {
|
owner.lockState(() {
|
||||||
_doLayout(constraints);
|
_doLayout(constraints);
|
||||||
}, building: true);
|
}, building: true);
|
||||||
}
|
}
|
||||||
|
@ -157,7 +157,7 @@ abstract class VirtualViewportElement extends RenderObjectElement {
|
|||||||
assert(startOffsetBase != null);
|
assert(startOffsetBase != null);
|
||||||
assert(startOffsetLimit != null);
|
assert(startOffsetLimit != null);
|
||||||
_updatePaintOffset();
|
_updatePaintOffset();
|
||||||
BuildableElement.lockState(_materializeChildren, building: true);
|
owner.lockState(_materializeChildren, building: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _materializeChildren() {
|
void _materializeChildren() {
|
||||||
|
163
packages/flutter/test/widget/independent_widget_layout_test.dart
Normal file
163
packages/flutter/test/widget/independent_widget_layout_test.dart
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
// Copyright 2016 The Chromium 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_test/flutter_test.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
const Size _kTestViewSize = const Size(800.0, 600.0);
|
||||||
|
|
||||||
|
class OffscreenRenderView extends RenderView {
|
||||||
|
OffscreenRenderView() {
|
||||||
|
configuration = new ViewConfiguration(size: _kTestViewSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void scheduleInitialFrame() {
|
||||||
|
scheduleInitialLayout();
|
||||||
|
scheduleInitialPaint(new TransformLayer(transform: new Matrix4.identity()));
|
||||||
|
// Don't call Scheduler.instance.ensureVisualUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void compositeFrame() {
|
||||||
|
// Don't draw to ui.window
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OffscreenWidgetTree {
|
||||||
|
OffscreenWidgetTree() {
|
||||||
|
renderView.attach(pipelineOwner);
|
||||||
|
renderView.scheduleInitialFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
final RenderView renderView = new OffscreenRenderView();
|
||||||
|
final BuildOwner buildOwner = new BuildOwner();
|
||||||
|
final PipelineOwner pipelineOwner = new PipelineOwner();
|
||||||
|
RenderObjectToWidgetElement<RenderBox> root;
|
||||||
|
|
||||||
|
void pumpWidget(Widget app) {
|
||||||
|
root = new RenderObjectToWidgetAdapter<RenderBox>(
|
||||||
|
container: renderView,
|
||||||
|
debugShortDescription: '[root]',
|
||||||
|
child: app
|
||||||
|
).attachToRenderTree(buildOwner, root);
|
||||||
|
pumpFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
void pumpFrame() {
|
||||||
|
buildOwner.buildDirtyElements();
|
||||||
|
pipelineOwner.flushLayout();
|
||||||
|
pipelineOwner.flushCompositingBits();
|
||||||
|
pipelineOwner.flushPaint();
|
||||||
|
renderView.compositeFrame();
|
||||||
|
if (SemanticsNode.hasListeners) {
|
||||||
|
pipelineOwner.flushSemantics();
|
||||||
|
SemanticsNode.sendSemanticsTree();
|
||||||
|
}
|
||||||
|
buildOwner.finalizeTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Counter {
|
||||||
|
int count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Trigger {
|
||||||
|
VoidCallback callback;
|
||||||
|
void fire() {
|
||||||
|
if (callback != null)
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TriggerableWidget extends StatefulWidget {
|
||||||
|
TriggerableWidget({ this.trigger, this.counter });
|
||||||
|
final Trigger trigger;
|
||||||
|
final Counter counter;
|
||||||
|
@override
|
||||||
|
TriggerableState createState() => new TriggerableState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class TriggerableState extends State<TriggerableWidget> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
config.trigger.callback = this.fire;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateConfig(TriggerableWidget oldConfig) {
|
||||||
|
config.trigger.callback = this.fire;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _count = 0;
|
||||||
|
void fire() {
|
||||||
|
setState(() {
|
||||||
|
_count++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
config.counter.count++;
|
||||||
|
return new Text("Bang $_count!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('no crosstalk between widget build owners', () {
|
||||||
|
testWidgets((WidgetTester tester) {
|
||||||
|
Trigger trigger1 = new Trigger();
|
||||||
|
Counter counter1 = new Counter();
|
||||||
|
Trigger trigger2 = new Trigger();
|
||||||
|
Counter counter2 = new Counter();
|
||||||
|
OffscreenWidgetTree tree = new OffscreenWidgetTree();
|
||||||
|
// Both counts should start at zero
|
||||||
|
expect(counter1.count, equals(0));
|
||||||
|
expect(counter2.count, equals(0));
|
||||||
|
// Lay out the "onscreen" in the default test binding
|
||||||
|
tester.pumpWidget(new TriggerableWidget(trigger: trigger1, counter: counter1));
|
||||||
|
// Only the "onscreen" widget should have built
|
||||||
|
expect(counter1.count, equals(1));
|
||||||
|
expect(counter2.count, equals(0));
|
||||||
|
// Lay out the "offscreen" in a separate tree
|
||||||
|
tree.pumpWidget(new TriggerableWidget(trigger: trigger2, counter: counter2));
|
||||||
|
// Now both widgets should have built
|
||||||
|
expect(counter1.count, equals(1));
|
||||||
|
expect(counter2.count, equals(1));
|
||||||
|
// Mark both as needing layout
|
||||||
|
trigger1.fire();
|
||||||
|
trigger2.fire();
|
||||||
|
// Marking as needing layout shouldn't immediately build anything
|
||||||
|
expect(counter1.count, equals(1));
|
||||||
|
expect(counter2.count, equals(1));
|
||||||
|
// Pump the "onscreen" layout
|
||||||
|
tester.pump();
|
||||||
|
// Only the "onscreen" widget should have rebuilt
|
||||||
|
expect(counter1.count, equals(2));
|
||||||
|
expect(counter2.count, equals(1));
|
||||||
|
// Pump the "offscreen" layout
|
||||||
|
tree.pumpFrame();
|
||||||
|
// Now both widgets should have rebuilt
|
||||||
|
expect(counter1.count, equals(2));
|
||||||
|
expect(counter2.count, equals(2));
|
||||||
|
// Mark both as needing layout, again
|
||||||
|
trigger1.fire();
|
||||||
|
trigger2.fire();
|
||||||
|
// Now pump the "offscreen" layout first
|
||||||
|
tree.pumpFrame();
|
||||||
|
// Only the "offscreen" widget should have rebuilt
|
||||||
|
expect(counter1.count, equals(2));
|
||||||
|
expect(counter2.count, equals(3));
|
||||||
|
// Pump the "onscreen" layout
|
||||||
|
tester.pump();
|
||||||
|
// Now both widgets should have rebuilt
|
||||||
|
expect(counter1.count, equals(3));
|
||||||
|
expect(counter2.count, equals(3));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -38,9 +38,9 @@ class _SteppedWidgetFlutterBinding extends WidgetFlutterBinding {
|
|||||||
// Pump the rendering pipeline up to the given phase.
|
// Pump the rendering pipeline up to the given phase.
|
||||||
@override
|
@override
|
||||||
void beginFrame() {
|
void beginFrame() {
|
||||||
buildDirtyElements();
|
buildOwner.buildDirtyElements();
|
||||||
_beginFrame();
|
_beginFrame();
|
||||||
Element.finalizeTree();
|
buildOwner.finalizeTree();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cloned from Renderer.beginFrame() but with early-exit semantics.
|
// Cloned from Renderer.beginFrame() but with early-exit semantics.
|
||||||
@ -84,7 +84,7 @@ class WidgetTester extends Instrumentation {
|
|||||||
final FakeAsync async;
|
final FakeAsync async;
|
||||||
final Clock clock;
|
final Clock clock;
|
||||||
|
|
||||||
/// Calls [runApp()] with the given widget, then triggers a frame sequent and
|
/// Calls [runApp()] with the given widget, then triggers a frame sequence and
|
||||||
/// flushes microtasks, by calling [pump()] with the same duration (if any).
|
/// flushes microtasks, by calling [pump()] with the same duration (if any).
|
||||||
/// The supplied EnginePhase is the final phase reached during the pump pass;
|
/// The supplied EnginePhase is the final phase reached during the pump pass;
|
||||||
/// if not supplied, the whole pass is executed.
|
/// if not supplied, the whole pass is executed.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user