提交 8afda4a5 编写于 作者: H Hixie

Handle Widget instances being moved as-is to different parts of the tree.

上级 0d476694
......@@ -212,6 +212,15 @@ abstract class Widget {
/// The parent of this widget in the widget tree.
Widget get parent => _parent;
// The "generation" of a Widget is the frame in which it was last
// synced. We use this to tell if an instance of a Widget has moved
// to earlier in the tree so that when we come across where it used
// to be, we pretend it was never there. See syncChild().
static int _currentGeneration = 1;
int _generation = 0;
bool get isFromOldGeneration => _generation < _currentGeneration;
void _markAsFromCurrentGeneration() { _generation = _currentGeneration; }
bool _mounted = false;
bool _wasMounted = false;
bool get mounted => _mounted;
......@@ -305,6 +314,11 @@ abstract class Widget {
bool retainStatefulNodeIfPossible(Widget newNode) => false;
void _sync(Widget old, dynamic slot) {
assert(isFromOldGeneration);
assert(old == null || old.isFromOldGeneration);
_markAsFromCurrentGeneration();
if (old != null && old != this)
old._markAsFromCurrentGeneration();
if (key is GlobalKey)
(key as GlobalKey)._didSync(); // TODO(ianh): Remove the cast once the analyzer is cleverer.
}
......@@ -342,35 +356,63 @@ abstract class Widget {
}
void remove() {
walkChildren((Widget child) => child.remove());
walkChildren((Widget child) {
if (child._generation <= _generation)
child.remove();
});
_renderObject = null;
setParent(null);
}
void detachRenderObject();
Widget _getCandidateSingleChildFrom(Widget oldChild) {
Widget candidate = oldChild.singleChild;
if (candidate != null && !candidate.isFromOldGeneration)
candidate = null;
assert(candidate == null || candidate.parent == oldChild);
return candidate;
}
// Returns the child which should be retained as the child of this node.
Widget syncChild(Widget newNode, Widget oldNode, dynamic slot) {
assert(() {
'You have probably used a single instance of a Widget in two different places in the widget tree. Widgets can only be used in one place at a time.';
return newNode == null || newNode.isFromOldGeneration;
});
if (oldNode != null && !oldNode.isFromOldGeneration)
oldNode = null;
if (newNode == oldNode) {
assert(newNode == null || newNode.mounted);
assert(newNode == null || newNode.isFromOldGeneration);
assert(newNode is! RenderObjectWrapper ||
(newNode is RenderObjectWrapper && newNode._ancestor != null)); // TODO(ianh): Simplify this once the analyzer is cleverer
if (newNode != null)
if (newNode != null) {
newNode.setParent(this);
newNode._markAsFromCurrentGeneration();
}
return newNode; // Nothing to do. Subtrees must be identical.
}
if (newNode == null) {
// the child in this slot has gone away
// the child in this slot has gone away (we know oldNode != null)
assert(oldNode != null);
assert(oldNode.isFromOldGeneration);
assert(oldNode.mounted);
oldNode.detachRenderObject();
oldNode.remove();
assert(!oldNode.mounted);
// we don't update the generation of oldNode, because there's
// still a chance it could be reused as-is later in the tree.
return null;
}
if (oldNode != null) {
assert(newNode != null);
assert(newNode.isFromOldGeneration);
assert(oldNode.isFromOldGeneration);
if (!_canSync(newNode, oldNode)) {
assert(oldNode.mounted);
// We want to handle the case where there is a removal of zero
......@@ -378,8 +420,7 @@ abstract class Widget {
// ourselves with a Widget that is a descendant of the
// oldNode, skipping the nodes in between. Let's try that.
Widget deadNode = oldNode;
Widget candidate = oldNode.singleChild;
assert(candidate == null || candidate.parent == oldNode);
Widget candidate = _getCandidateSingleChildFrom(oldNode);
oldNode = null;
while (candidate != null && _shouldReparentDuringSync) {
if (_canSync(newNode, candidate)) {
......@@ -389,13 +430,15 @@ abstract class Widget {
oldNode = candidate;
break;
}
assert(candidate.singleChild == null || candidate.singleChild.parent == candidate);
candidate = candidate.singleChild;
candidate = _getCandidateSingleChildFrom(candidate);
}
deadNode.detachRenderObject();
deadNode.remove();
}
if (oldNode != null) {
assert(newNode.isFromOldGeneration);
assert(oldNode.isFromOldGeneration);
assert(_canSync(newNode, oldNode));
if (oldNode.retainStatefulNodeIfPossible(newNode)) {
assert(oldNode.mounted);
assert(!newNode.mounted);
......@@ -410,7 +453,6 @@ abstract class Widget {
}
assert(oldNode == null || (oldNode.mounted == false && oldNode.parent == null));
assert(!newNode.mounted);
newNode.setParent(this);
newNode._sync(oldNode, slot);
assert(newNode.renderObject is RenderObject);
......@@ -439,7 +481,13 @@ abstract class Widget {
nextPrefix = prefix + ' ';
childrenString += lastChild.toString(nextPrefix, _adjustPrefixWithParentCheck(lastChild, nextStartPrefix));
}
return '$startPrefix${toStringName()}\n$childrenString';
String suffix = '';
if (_generation != _currentGeneration) {
int delta = _generation - _currentGeneration;
String sign = delta < 0 ? '' : '+';
suffix = ' gen$sign$delta';
}
return '$startPrefix${toStringName()}$suffix\n$childrenString';
}
String toStringName() {
if (key == null)
......@@ -465,7 +513,9 @@ abstract class Widget {
}
bool _canSync(Widget a, Widget b) {
return a.runtimeType == b.runtimeType && a.key == b.key;
return a.runtimeType == b.runtimeType &&
a.key == b.key &&
(a._generation == 0 || b._generation == 0);
}
......@@ -786,6 +836,7 @@ abstract class Component extends Widget {
void _buildIfDirty() {
if (!_dirty || !_mounted)
return;
assert(isFromOldGeneration);
assert(renderObject != null);
_sync(null, _slot);
}
......@@ -900,7 +951,7 @@ void exitLayoutCallbackBuilder(LayoutCallbackBuilderHandle handle) {
_inLayoutCallbackBuilder -= 1;
return true;
});
Widget._notifyMountStatusChanged();
_endSyncPhase();
}
List<int> _debugFrameTimes = <int>[];
......@@ -942,8 +993,7 @@ void _buildDirtyComponents() {
_inBuildDirtyComponents = false;
sky.tracing.end('Component.flushBuild');
}
Widget._notifyMountStatusChanged();
_endSyncPhase();
}
if (_shouldLogRenderDuration) {
......@@ -956,6 +1006,12 @@ void _buildDirtyComponents() {
_debugFrameTimes.clear();
}
}
}
void _endSyncPhase() {
Widget._currentGeneration += 1;
Widget._notifyMountStatusChanged();
}
void _scheduleComponentForRender(Component component) {
......@@ -1111,9 +1167,10 @@ abstract class RenderObjectWrapper extends Widget {
// top of the lists
while ((childrenTop <= oldChildrenBottom) && (childrenTop <= newChildrenBottom)) {
Widget oldChild = oldChildren[childrenTop];
if (!oldChild.isFromOldGeneration)
break;
assert(oldChild.mounted);
Widget newChild = newChildren[childrenTop];
assert(newChild == oldChild || !newChild.mounted);
if (!_canSync(oldChild, newChild))
break;
childrenTop += 1;
......@@ -1124,9 +1181,10 @@ abstract class RenderObjectWrapper extends Widget {
// bottom of the lists
while ((childrenTop <= oldChildrenBottom) && (childrenTop <= newChildrenBottom)) {
Widget oldChild = oldChildren[oldChildrenBottom];
if (!oldChild.isFromOldGeneration)
break;
assert(oldChild.mounted);
Widget newChild = newChildren[newChildrenBottom];
assert(newChild == oldChild || !newChild.mounted);
if (!_canSync(oldChild, newChild))
break;
newChild = syncChild(newChild, oldChild, nextSibling);
......@@ -1145,13 +1203,15 @@ abstract class RenderObjectWrapper extends Widget {
oldKeyedChildren = new Map<Key, Widget>();
while (childrenTop <= oldChildrenBottom) {
Widget oldChild = oldChildren[oldChildrenBottom];
assert(oldChild.mounted);
if (oldChild.key != null) {
oldKeyedChildren[oldChild.key] = oldChild;
} else {
syncChild(null, oldChild, null);
if (oldChild.isFromOldGeneration) {
assert(oldChild.mounted);
if (oldChild.key != null) {
oldKeyedChildren[oldChild.key] = oldChild;
} else {
syncChild(null, oldChild, null);
}
oldChildrenBottom -= 1;
}
oldChildrenBottom -= 1;
}
}
......@@ -1163,7 +1223,7 @@ abstract class RenderObjectWrapper extends Widget {
Key key = newChild.key;
if (key != null) {
oldChild = oldKeyedChildren[newChild.key];
if (oldChild != null) {
if (oldChild != null && oldChild.isFromOldGeneration) {
if (oldChild.runtimeType != newChild.runtimeType)
oldChild = null;
oldKeyedChildren.remove(key);
......@@ -1186,8 +1246,9 @@ abstract class RenderObjectWrapper extends Widget {
childrenTop -= 1;
Widget oldChild = oldChildren[childrenTop];
assert(oldChild.mounted);
assert(oldChild.isFromOldGeneration);
Widget newChild = newChildren[childrenTop];
assert(newChild == oldChild || !newChild.mounted);
assert(newChild.isFromOldGeneration);
assert(_canSync(oldChild, newChild));
newChild = syncChild(newChild, oldChild, nextSibling);
assert(newChild.mounted);
......@@ -1198,7 +1259,8 @@ abstract class RenderObjectWrapper extends Widget {
if (haveOldNodes && !oldKeyedChildren.isEmpty) {
for (Widget oldChild in oldKeyedChildren.values)
syncChild(null, oldChild, null);
if (oldChild.isFromOldGeneration)
syncChild(null, oldChild, null);
}
assert(renderObject == this.renderObject); // TODO(ianh): Remove this once the analyzer is cleverer
......
......@@ -201,9 +201,15 @@ class MixedViewport extends RenderObjectWrapper {
Widget widget = builder(index);
assert(widget != null);
assert(widget.key != null);
assert(widget.isFromOldGeneration);
_Key key = new _Key.fromWidget(widget);
Widget oldWidget = _childrenByKey[key];
assert(oldWidget != null);
assert(() {
'One of the nodes that was in this MixedViewport was placed in another part of the tree, without the MixedViewport\'s token or builder being changed ' +
'and without the MixedViewport\'s MixedViewportLayoutState object being told about that any of the children were invalid.';
return oldWidget.isFromOldGeneration;
});
assert(oldWidget.renderObject.parent == renderObject);
widget = syncChild(widget, oldWidget, renderObject.childAfter(oldWidget.renderObject));
assert(widget != null);
......@@ -225,8 +231,11 @@ class MixedViewport extends RenderObjectWrapper {
Widget newWidget = builder(index);
assert(newWidget != null);
assert(newWidget.key != null);
assert(newWidget.isFromOldGeneration);
final _Key key = new _Key.fromWidget(newWidget);
Widget oldWidget = _childrenByKey[key];
if (oldWidget != null && !oldWidget.isFromOldGeneration)
oldWidget = null;
newWidget = syncChild(newWidget, oldWidget, _omit);
assert(newWidget != null);
// Update the offsets based on the newWidget's dimensions.
......@@ -254,6 +263,8 @@ class MixedViewport extends RenderObjectWrapper {
assert(widget.key != null); // items in lists must have keys
final _Key key = new _Key.fromWidget(widget);
Widget oldWidget = _childrenByKey[key];
if (oldWidget != null && !oldWidget.isFromOldGeneration)
oldWidget = null;
widget = syncChild(widget, oldWidget, _omit);
if (index >= offsets.length - 1) {
assert(index == offsets.length - 1);
......@@ -286,6 +297,18 @@ class MixedViewport extends RenderObjectWrapper {
layoutState._notifyListeners();
}
void _unsyncChild(Widget widget) {
assert(!widget.isFromOldGeneration);
// The following two lines are the equivalent of "syncChild(null,
// widget, null)", but actually doing that wouldn't work because
// widget is now from the new generation and so syncChild() would
// assume that that means someone else has already sync()ed with it
// and that it's wanted. But it's not wanted! We want to get rid of
// it. So we do it manually.
widget.detachRenderObject();
widget.remove();
}
void _doLayout(BoxConstraints constraints) {
Map<_Key, Widget> newChildren = new Map<_Key, Widget>();
Map<int, Widget> builtChildren = new Map<int, Widget>();
......@@ -334,7 +357,7 @@ class MixedViewport extends RenderObjectWrapper {
builtChildren[index] = widget;
} else {
childrenByKey.remove(widgetKey);
syncChild(null, widget, null);
_unsyncChild(widget);
}
}
}
......@@ -378,7 +401,7 @@ class MixedViewport extends RenderObjectWrapper {
break;
}
childrenByKey.remove(widgetKey);
syncChild(null, widget, null);
_unsyncChild(widget);
startIndex += 1;
assert(startIndex == offsets.length - 1);
}
......
......@@ -43,4 +43,29 @@ void main() {
expect(container.renderObject.parentData.bottom, isNull);
expect(container.renderObject.parentData.left, isNull);
});
test('Can remove parent data', () {
WidgetTester tester = new WidgetTester();
Container container;
tester.pumpFrame(() {
container = new Container(width: 10.0, height: 10.0);
return new Stack([ new Positioned(left: 10.0, child: container) ]);
});
expect(container.renderObject.parentData.top, isNull);
expect(container.renderObject.parentData.right, isNull);
expect(container.renderObject.parentData.bottom, isNull);
expect(container.renderObject.parentData.left, equals(10.0));
tester.pumpFrame(() {
return new Stack([ container ]);
});
expect(container.renderObject.parentData.top, isNull);
expect(container.renderObject.parentData.right, isNull);
expect(container.renderObject.parentData.bottom, isNull);
expect(container.renderObject.parentData.left, isNull);
});
}
......@@ -4,13 +4,15 @@ import 'package:test/test.dart';
import 'widget_tester.dart';
class TestState extends StatefulComponent {
TestState({this.child, this.state});
TestState({ this.child, this.persistentState, this.syncedState });
Widget child;
int state;
int persistentState;
int syncedState;
int syncs = 0;
void syncConstructorArguments(TestState source) {
child = source.child;
// we explicitly do NOT sync the state from the new instance
syncedState = source.syncedState;
// we explicitly do NOT sync the persistentState from the new instance
// because we're using that to track whether we got recreated
syncs += 1;
}
......@@ -29,7 +31,7 @@ void main() {
return new Container(
child: new Container(
child: new TestState(
state: 1,
persistentState: 1,
child: new Container()
)
)
......@@ -38,21 +40,21 @@ void main() {
TestState stateWidget = tester.findWidget((widget) => widget is TestState);
expect(stateWidget.state, equals(1));
expect(stateWidget.persistentState, equals(1));
expect(stateWidget.syncs, equals(0));
tester.pumpFrame(() {
return new Container(
child: new Container(
child: new TestState(
state: 2,
persistentState: 2,
child: new Container()
)
)
);
});
expect(stateWidget.state, equals(1));
expect(stateWidget.persistentState, equals(1));
expect(stateWidget.syncs, equals(1));
});
......@@ -92,4 +94,94 @@ void main() {
//
// });
test('swap instances around', () {
WidgetTester tester = new WidgetTester();
Widget a, b;
tester.pumpFrame(() {
a = new TestState(persistentState: 0x61, syncedState: 0x41, child: new Text('apple'));
b = new TestState(persistentState: 0x62, syncedState: 0x42, child: new Text('banana'));
return new Column([]);
});
GlobalKey keyA = new GlobalKey();
GlobalKey keyB = new GlobalKey();
TestState foundA, foundB;
tester.pumpFrame(() {
return new Column([
new Container(
key: keyA,
child: a
),
new Container(
key: keyB,
child: b
)
]);
});
foundA = (tester.findWidget((widget) => widget.key == keyA) as Container).child as TestState;
foundB = (tester.findWidget((widget) => widget.key == keyB) as Container).child as TestState;
expect(foundA, equals(a));
expect(foundA.persistentState, equals(0x61));
expect(foundA.syncedState, equals(0x41));
expect(foundB, equals(b));
expect(foundB.persistentState, equals(0x62));
expect(foundB.syncedState, equals(0x42));
tester.pumpFrame(() {
return new Column([
new Container(
key: keyA,
child: a
),
new Container(
key: keyB,
child: b
)
]);
});
foundA = (tester.findWidget((widget) => widget.key == keyA) as Container).child as TestState;
foundB = (tester.findWidget((widget) => widget.key == keyB) as Container).child as TestState;
// same as before
expect(foundA, equals(a));
expect(foundA.persistentState, equals(0x61));
expect(foundA.syncedState, equals(0x41));
expect(foundB, equals(b));
expect(foundB.persistentState, equals(0x62));
expect(foundB.syncedState, equals(0x42));
// now we swap the nodes over
// since they are both "old" nodes, they shouldn't sync with each other even though they look alike
tester.pumpFrame(() {
return new Column([
new Container(
key: keyA,
child: b
),
new Container(
key: keyB,
child: a
)
]);
});
foundA = (tester.findWidget((widget) => widget.key == keyA) as Container).child as TestState;
foundB = (tester.findWidget((widget) => widget.key == keyB) as Container).child as TestState;
expect(foundA, equals(b));
expect(foundA.persistentState, equals(0x62));
expect(foundA.syncedState, equals(0x42));
expect(foundB, equals(a));
expect(foundB.persistentState, equals(0x61));
expect(foundB.syncedState, equals(0x41));
});
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册