collapse method

void collapse({
  1. required TKey key,
  2. bool animate = true,
})

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});
}