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 = _operationGroups[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 at the OLD curvedValue) and set
// startExtent to that value, targetExtent to full. Reset
// the controller to value=0 and forward(); the group
// replays each row from `current → full` over the full
// configured duration, no jump.
// 2. Clear pendingRemoval so the dismissed handler doesn't
// yank rows.
//
// Resetting `controller.value = 0` fires the `dismissed` status
// synchronously, which the group's status listener would
// otherwise treat as a real collapse-complete and dispose the
// group. The listener has an identity guard
// (`identical(_operationGroups[key], group)`), so we briefly
// detach the group from `_operationGroups` around the value=0
// store, let the gated dismissed event fire harmlessly, then
// re-attach for the actual `forward()` call.
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;
}
_operationGroups.remove(key);
try {
existingGroup.controller.value = 0.0;
} finally {
_operationGroups[key] = existingGroup;
}
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
final nodesToShow = _flattenSubtree(key, includeRoot: false);
final controller = AnimationController(
vsync: _vsync,
duration: animationDuration,
value: 0.0,
);
final group = OperationGroup<TKey>(
controller: controller,
curve: animationCurve,
operationKey: key,
);
_installOperationGroup(key, group);
// 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++;
controller.forward();
_ensureStandaloneTickerRunning();
_notifyStructural(affectedKeys: <TKey>{key});
}