expand method
Expands the given node, revealing its children.
Implementation
void expand({required TKey key, bool animate = true}) {
if (animationDuration == Duration.zero) animate = false;
if (!_hasKey(key)) {
return;
}
if (_isExpandedKey(key)) {
return;
}
final children = _childListOf(key);
if (children == null || children.isEmpty) {
return;
}
// Don't expand if this node is currently exiting
if (isExiting(key)) {
return;
}
// If ancestors are collapsed, just record the expansion state.
// The node is not visible, so there is nothing to animate or
// insert into the visible order. When ancestors are later expanded,
// this node's children will appear immediately.
if (!_ancestorsExpandedFast(key)) {
_setExpandedKey(key, true);
_notifyStructural(affectedKeys: <TKey>{key});
return;
}
_setExpandedKey(key, true);
// Find where to insert children in visible order
final parentIndex = _order.indexOf(key);
if (parentIndex == VisibleOrderBuffer.kNotVisible) {
return;
}
if (!animate) {
// No animation — insert and return
final nodesToShow = _flattenSubtree(key, includeRoot: false);
final nodesToInsert = <TKey>[];
for (final nodeId in nodesToShow) {
if (_isPendingDeletion(nodeId)) continue;
if (!_order.contains(nodeId)) {
nodesToInsert.add(nodeId);
} else {
_removeAnimation(nodeId);
}
}
if (nodesToInsert.isNotEmpty) {
final insertIndex = parentIndex + 1;
_order.insertAllKeys(insertIndex, nodesToInsert);
_updateIndicesFrom(insertIndex);
}
_structureGeneration++;
_notifyStructural(affectedKeys: <TKey>{key});
return;
}
// Animated expand
final existingGroup = _opGroupAt(key);
if (existingGroup != null) {
// Path 1: Reversing a collapse — group already exists.
//
// The op-group's controller is the shared timing primitive;
// each member's NodeGroupExtent envelope (start/target) defines
// what it animates between. To reverse the collapse smoothly:
// 1. Rebase each member: capture its current visual extent under
// the OLD envelope, set startExtent = current, targetExtent =
// full. Reset value=0 and forward().
// 2. Clear pendingRemoval so the dismissed handler doesn't yank.
//
// Resetting controller.value=0 fires a synchronous dismissed event;
// the registry's install-time identity guard ignores it if the
// group has been briefly detached. runWithGroupDetached encapsulates
// the detach/reattach pattern.
if (existingGroup.pendingRemoval.isNotEmpty) {
existingGroup.pendingRemoval.clear();
_bumpAnimGen();
}
final preReversalCurvedValue = existingGroup.curvedValue;
for (final entry in existingGroup.members.entries) {
final full = _fullExtentOf(entry.key) ?? defaultExtent;
final currentExtent = entry.value.computeExtent(
preReversalCurvedValue,
full,
);
entry.value.startExtent = currentExtent;
entry.value.targetExtent = full;
}
_anim.opGroups.runWithGroupDetached(key, (group) {
group.controller.value = 0.0;
});
existingGroup.controller.forward();
// Handle descendants NOT in this group (from nested expansions)
final nodesToShow = _flattenSubtree(key, includeRoot: false);
for (final nodeId in nodesToShow) {
if (_isPendingDeletion(nodeId)) continue;
if (existingGroup.members.containsKey(nodeId)) continue;
if (_standaloneAt(nodeId) case final anim?
when anim.type == AnimationType.exiting) {
// Reverse the exit to an enter with speedMultiplier
_startStandaloneEnterAnimation(nodeId);
} else if (!_order.contains(nodeId)) {
// New node not yet visible — insert at correct sibling position
// and animate. _insertNodeIntoVisibleOrder appends at the end of
// the grandparent's subtree, which drops the node past its
// following siblings when they are already in the visible order.
_insertNewNodeAmongSiblings(nodeId);
_startStandaloneEnterAnimation(nodeId);
}
}
_structureGeneration++;
_notifyStructural(affectedKeys: <TKey>{key});
return;
}
// Path 2: Fresh expand — create new operation group via the
// OperationGroupRegistry's install API (constructs the controller +
// OperationGroup internally and wires the tick + status callbacks).
final nodesToShow = _flattenSubtree(key, includeRoot: false);
final group = _anim.opGroups.install(key, animationCurve);
_bumpAnimGen(); // matches today's `_installOperationGroup` body
// Fast path check: count new vs existing nodes
int newNodeCount = 0;
int effectiveCount = 0;
for (final nodeId in nodesToShow) {
if (_isPendingDeletion(nodeId)) continue;
effectiveCount++;
if (!_order.contains(nodeId)) {
newNodeCount++;
}
}
// Path 2 expand: each newly-affected descendant joins the group as
// a member with a NodeGroupExtent envelope. Per-node animation
// records are NOT created on a fresh op — they only come into
// existence on CAPTURE (a prior op's mid-flight node being pulled
// into a new op). The op-group's controller is the shared timing
// primitive for non-captured members; private records, when they
// exist on captured members, carry the captured node's own clock.
if (newNodeCount == 0) {
// All nodes already visible (reversing collapse animation)
for (final nodeId in nodesToShow) {
if (_isPendingDeletion(nodeId)) continue;
final capturedExtent = _captureAndRemoveFromGroups(nodeId);
final nge = NodeGroupExtent(
startExtent: capturedExtent ?? 0.0,
targetExtent: _fullExtentOf(nodeId) ?? _unknownExtent,
);
group.members[nodeId] = nge;
_setOperationGroup(nodeId, key);
}
} else if (newNodeCount == effectiveCount) {
// All nodes need insertion (normal expand)
final nodesToInsert = <TKey>[];
for (final nodeId in nodesToShow) {
if (_isPendingDeletion(nodeId)) continue;
final capturedExtent = _captureAndRemoveFromGroups(nodeId);
final nge = NodeGroupExtent(
startExtent: capturedExtent ?? 0.0,
targetExtent: _fullExtentOf(nodeId) ?? _unknownExtent,
);
group.members[nodeId] = nge;
_setOperationGroup(nodeId, key);
nodesToInsert.add(nodeId);
}
final insertIndex = parentIndex + 1;
_order.insertAllKeys(insertIndex, nodesToInsert);
_updateIndicesFrom(insertIndex);
} else {
// Mixed path: some visible (exiting), some need insertion
int currentInsertIndex = parentIndex + 1;
int insertOffset = 0;
int minInsertIndex = _order.length;
for (final nodeId in nodesToShow) {
if (_isPendingDeletion(nodeId)) continue;
final existingIndex = _order.indexOf(nodeId);
final capturedExtent = _captureAndRemoveFromGroups(nodeId);
final nge = NodeGroupExtent(
startExtent: capturedExtent ?? 0.0,
targetExtent: _fullExtentOf(nodeId) ?? _unknownExtent,
);
group.members[nodeId] = nge;
_setOperationGroup(nodeId, key);
if (existingIndex != VisibleOrderBuffer.kNotVisible) {
// Node already visible (was exiting)
currentInsertIndex = existingIndex + insertOffset + 1;
} else {
// Insert at current position
if (currentInsertIndex < minInsertIndex) {
minInsertIndex = currentInsertIndex;
}
_order.insertKey(currentInsertIndex, nodeId);
insertOffset++;
currentInsertIndex++;
}
}
if (insertOffset > 0) {
for (int i = minInsertIndex; i < _order.length; i++) {
_order.setIndexByNid(_order.orderNids[i], i);
}
_assertIndexConsistency();
}
}
_structureGeneration++;
group.controller.forward();
_anim.standalone.ensureRunning();
_notifyStructural(affectedKeys: <TKey>{key});
}