collapseAll method

void collapseAll({
  1. bool animate = true,
  2. int? maxDepth,
})

Collapses all nodes in the tree.

Uses batch operations for better performance with large trees.

Implementation

void collapseAll({bool animate = true, int? maxDepth}) {
  if (_animationStyle.expandCollapse.duration == Duration.zero) {
    animate = false;
  }
  // Flush any pending visible-order rebuild from a prior in-batch mutator.
  // `_getVisibleDescendants` below reads `_order.contains`; inside a
  // [runBatch] the order still reflects state at batch entry, so a node
  // made visible by an earlier in-batch mutation would be missed,
  // never joining the bulk group and popping out in one frame. Same
  // pattern as [insert] / [expand] / [collapse].
  _ensureVisibleOrder();
  // Collect all expanded nodes and their visible descendants
  final nodesToCollapse = <TKey>[];
  final nodesToHide = <TKey>[];
  for (final rootId in _roots) {
    if (_isExpandedKey(rootId)) {
      nodesToCollapse.add(rootId);
      nodesToHide.addAll(_getVisibleDescendants(rootId));
    }
  }
  // Also check for nodes that are entering (from an interrupted expandAll)
  final nodesToHideSet = nodesToHide.toSet();

  // Check standalone entering animations
  for (final nid in _anim.standalone.activeNids) {
    final state = _anim.standalone.slotAtNid(nid)!;
    if (state.type != AnimationType.entering) continue;
    final key = _nids.keyOfUnchecked(nid);
    if (nodesToHideSet.contains(key)) continue;
    if (_parentKeyOfKey(key) != null) {
      nodesToHide.add(key);
      nodesToHideSet.add(key);
    }
  }

  // Check operation group members (expanding)
  for (final entry in _opGroupEntries) {
    final group = entry.value;
    // (loop body uses `group`)
    if (group.pendingRemoval.isEmpty) {
      // Group is expanding
      for (final key in group.members.keys) {
        if (!nodesToHideSet.contains(key)) {
          if (_parentKeyOfKey(key) != null) {
            nodesToHide.add(key);
            nodesToHideSet.add(key);
          }
        }
      }
    }
  }

  // Check bulk group members (expanding nodes)
  if (_activeBulkGroup != null) {
    for (final key in _activeBulkGroup!.members) {
      if (!nodesToHideSet.contains(key)) {
        if (_parentKeyOfKey(key) != null) {
          nodesToHide.add(key);
          nodesToHideSet.add(key);
        }
      }
    }
  }

  if (nodesToHide.isEmpty) {
    if (nodesToCollapse.isNotEmpty) {
      _recordExpansionChanges(
        _collapseAllInRegistry(maxDepth),
        wasExpanded: true,
      );
      // Bulk expansion-state clear: see main collapseAll branch below.
      _notifyStructural();
    }
    return;
  }
  // Clear expansion state for ALL nodes within depth limit,
  // not just visible ones.
  _recordExpansionChanges(
    _collapseAllInRegistry(maxDepth),
    wasExpanded: true,
  );
  _structureGeneration++;
  if (animate) {
    // Reverse expanding operation groups. Snapshot before iterating: a
    // group's controller already at lowerBound would fire `dismissed`
    // synchronously inside `reverse()`, removing itself from `_groups`
    // mid-iteration. See [_opGroupSnapshot] docs.
    bool opGroupReversed = false;
    _opGroupSnapshot
      ..clear()
      ..addAll(_opGroupEntries);
    for (final entry in _opGroupSnapshot) {
      final group = entry.value;
      if (group.pendingRemoval.isEmpty) {
        // Group is expanding: reverse it
        for (final nodeId in group.members.keys) {
          if (!_isPendingDeletion(nodeId)) {
            group.pendingRemoval.add(nodeId);
            opGroupReversed = true;
          }
        }
        // Normalize startExtent to 0 so the reversal terminates at
        // zero instead of at a captured mid-flight start value.
        for (final member in group.members.entries) {
          member.value.startExtent = 0.0;
        }
        group.controller.reverse();
      }
    }
    _opGroupSnapshot.clear();
    if (opGroupReversed) _bumpAnimGen();

    // Check if there's an expanding bulk animation we can reverse
    if (_activeBulkGroup != null &&
        _activeBulkGroup!.members.isNotEmpty &&
        _activeBulkGroup!.pendingRemoval.isEmpty) {
      // Mark all members for removal when animation completes at 0
      for (final key in _activeBulkGroup!.members) {
        if (!_isPendingDeletion(key)) {
          _addBulkPending(key);
        }
      }

      // Handle additional nodes not in any group
      for (final key in nodesToHide) {
        if (_isPendingDeletion(key)) continue;
        if (!_activeBulkGroup!.members.contains(key) &&
            !_hasOperationGroup(key)) {
          _startStandaloneExitAnimation(key);
        }
      }

      // Reverse the controller direction
      _activeBulkGroup!.controller.reverse();
      _bumpBulkGen();
    } else if (_activeBulkGroup != null &&
        _activeBulkGroup!.pendingRemoval.isNotEmpty) {
      // Continuation: a bulk collapse is already mid-flight. Keep the
      // group, existing members continue from their current extent.
      // Creating a fresh group at value 1.0 here (e.g. on a double-tap
      // of a "collapse all" button) would dispose the in-flight one and
      // snap every half-collapsed row back to full extent for a frame.
      //
      // Genuinely NEW nodes must NOT join the mid-flight group (they
      // would jump straight to `full * currentValue`); route them
      // through standalone exit animations instead.
      for (final key in nodesToHide) {
        if (_isPendingDeletion(key)) continue;
        if (_hasOperationGroup(key)) continue;
        if (_activeBulkGroup!.members.contains(key) ||
            _activeBulkGroup!.pendingRemoval.contains(key)) {
          continue;
        }
        final st = _standaloneAt(key);
        if (st != null && st.type == AnimationType.exiting) {
          continue;
        }
        _startStandaloneExitAnimation(key);
      }
      _bumpBulkGen();
    } else {
      // Create fresh group via the BulkAnimator with value=1.0 (collapse
      // starts fully expanded and reverses toward 0).
      _anim.bulk.createGroup(
        _animationStyle.expandCollapse.duration,
        _animationStyle.expandCollapse.curve,
        initialValue: 1.0,
      );
      _bumpBulkGen();

      // Add nodes to the bulk group, keeping individually-animating
      // nodes on their own timeline for smooth transitions.
      for (final key in nodesToHide) {
        if (_isPendingDeletion(key)) continue;
        if (_hasOperationGroup(key)) continue;
        if (_hasStandalone(key)) {
          // Reverse standalone animation smoothly
          _startStandaloneExitAnimation(key);
        } else {
          _removeAnimation(key);
          _addBulkMember(key);
          _addBulkPending(key);
        }
      }

      // Start collapsing (value 1 -> 0)
      if (_activeBulkGroup!.members.isNotEmpty) {
        _activeBulkGroup!.controller.reverse();
      }
      _bumpBulkGen();
    }
  } else {
    // Remove immediately
    final toRemove = <TKey>{};
    for (final key in nodesToHide) {
      if (!_isPendingDeletion(key)) {
        toRemove.add(key);
        _removeAnimation(key);
      }
    }
    if (toRemove.isNotEmpty) {
      _removeFromVisibleOrder(toRemove);
    }
  }
  // Bulk expansion-state clear: every node whose isExpanded state flipped
  // may render differently (chevron rotation, etc.). The set can span
  // arbitrary subtrees; fall back to a full refresh.
  _notifyStructural();
}