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 (_animationStyle.expandCollapse.duration == Duration.zero) {
    animate = false;
  }
  if (!_hasKey(key) || !_isExpandedKey(key)) {
    return;
  }
  // See [insert] for the rationale: flush any deferred visible-order
  // rebuild so the descendant / index lookups below operate on fresh
  // state.
  _ensureVisibleOrder();
  // Find all visible descendants (includes nodes currently entering).
  // Computed ONCE and shared by baseline staging and the removal logic
  // below. Walking here, BEFORE `_setExpandedKey` flips the node, returns
  // the same list a post-flip walk would: `_getVisibleDescendantsInto`
  // deliberately does not gate on [key]'s own expansion flag (see its
  // declaration doc), and nothing between here and the removal logic
  // mutates the visible order (`_setExpandedKey` is store-only).
  final descendants = _getVisibleDescendants(key);
  // Symmetric case: if any visible descendant about to LEAVE visible
  // order currently has a live entry-slide/ghost, stage a slide baseline
  // FIRST (first-wins), capturing pre-collapse painted positions. Captured
  // from the still-current visible order BEFORE `_setExpandedKey` flips the
  // node, so the descendants are still visible here. Mirrors the expand
  // path; the O(1) gate inside the helper keeps idle collapses probe-free.
  _stageSlideBaselineForBaseChange(() => descendants);
  _setExpandedKey(key, false);
  _recordExpansionChange(key, wasExpanded: true);
  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 = _opGroupAt(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` to 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 to 0, lerp(0, current, value)
    //      runs from current to 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);
    }
    _anim.opGroups.runWithGroupDetached(key, (group) {
      group.controller.value = 1.0;
    });
    _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 via the
  // OperationGroupRegistry's install API with initialValue=1.0
  // (collapse starts fully expanded and reverses toward 0).
  final group = _anim.opGroups.install(
    key,
    _animationStyle.expandCollapse.curve,
    initialValue: 1.0,
  );
  _bumpAnimGen(); // invalidate animation-generation-keyed caches

  for (final nodeId in descendants) {
    if (_isPendingDeletion(nodeId)) continue;
    final capturedExtent = _captureAndRemoveFromGroups(nodeId);
    final nge = NodeGroupExtent(
      startExtent: 0.0,
      targetExtent:
          capturedExtent ?? (_fullExtentOf(nodeId) ?? defaultExtent),
      targetIsCaptured: capturedExtent != null,
    );
    group.members[nodeId] = nge;
    group.pendingRemoval.add(nodeId);
    _setOperationGroup(nodeId, key);
  }

  _structureGeneration++;
  group.controller.reverse();
  // See the matching gate in the expand path.
  if (_anim.standalone.hasAny) {
    _anim.standalone.ensureRunning();
  }
  _notifyStructural(affectedKeys: <TKey>{key});
}