collapse method
Collapses the given node, hiding its children.
Note: This preserves the expansion state of descendant nodes. When the node is re-expanded, any previously expanded children will also show their children automatically.
Implementation
void collapse({required TKey key, bool animate = true}) {
if (animationDuration == Duration.zero) animate = false;
if (!_hasKey(key) || !_isExpandedKey(key)) {
return;
}
_setExpandedKey(key, false);
// Find all visible descendants (includes nodes currently entering)
final descendants = _getVisibleDescendants(key);
if (descendants.isEmpty) {
_notifyStructural(affectedKeys: <TKey>{key});
return;
}
if (!animate) {
// Remove immediately from visible order
final toRemove = <TKey>{};
for (final nodeId in descendants) {
if (!_isPendingDeletion(nodeId)) {
toRemove.add(nodeId);
_removeAnimation(nodeId);
}
}
if (toRemove.isNotEmpty) {
_removeFromVisibleOrder(toRemove);
_structureGeneration++;
}
_notifyStructural(affectedKeys: <TKey>{key});
return;
}
// Animated collapse
final existingGroup = _operationGroups[key];
if (existingGroup != null) {
// Path 1: Reversing an expand — group already exists.
//
// Mirror of the expand Path-1 reversal block:
// 1. Rebase each member: capture its current visual extent
// and animate from `current → 0` over the configured
// duration. We do this by setting startExtent=0 and
// targetExtent=currentExtent (so as the controller's
// reverse takes value from 1 → 0, lerp(0, current, value)
// runs from current → 0).
// 2. Add all members to pendingRemoval so the dismissed
// handler removes them from `_order`.
// 3. Reset controller.value=1 with the detach/reattach trick
// so the reverse plays over full duration with no jump.
//
// Captured members' private records re-pause as their nodes
// re-enter pendingRemoval; their preserved progress is unchanged
// and they'll resume only on a subsequent re-expand.
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 = 0.0;
entry.value.targetExtent = currentExtent;
existingGroup.pendingRemoval.add(entry.key);
}
_operationGroups.remove(key);
try {
existingGroup.controller.value = 1.0;
} finally {
_operationGroups[key] = existingGroup;
}
_bumpAnimGen();
existingGroup.controller.reverse();
// Handle descendants NOT in this group (from nested expansions)
for (final nodeId in descendants) {
if (_isPendingDeletion(nodeId)) continue;
if (existingGroup.members.containsKey(nodeId)) continue;
// Create standalone exit animation with speedMultiplier
_startStandaloneExitAnimation(nodeId, triggeringAncestorId: key);
}
_structureGeneration++;
_notifyStructural(affectedKeys: <TKey>{key});
return;
}
// Path 2: Fresh collapse — create new operation group
final controller = AnimationController(
vsync: _vsync,
duration: animationDuration,
value: 1.0,
);
final group = OperationGroup<TKey>(
controller: controller,
curve: animationCurve,
operationKey: key,
);
_installOperationGroup(key, group);
for (final nodeId in descendants) {
if (_isPendingDeletion(nodeId)) continue;
final capturedExtent = _captureAndRemoveFromGroups(nodeId);
final nge = NodeGroupExtent(
startExtent: 0.0,
targetExtent:
capturedExtent ?? (_fullExtentOf(nodeId) ?? defaultExtent),
// When this member's target was set from a captured visual
// extent, freeze it: a later setFullExtent resize must not
// retroactively expand the row to its natural full size
// mid-collapse (the "children appear all at once at full"
// bug). When the target was the natural full reference (no
// capture), resize updates are still welcome.
targetIsCaptured: capturedExtent != null,
);
group.members[nodeId] = nge;
group.pendingRemoval.add(nodeId);
_setOperationGroup(nodeId, key);
}
_structureGeneration++;
controller.reverse();
_ensureStandaloneTickerRunning();
_notifyStructural(affectedKeys: <TKey>{key});
}