expandAll method
Expands all nodes in the tree.
Uses batch operations for better performance with large trees.
Implementation
void expandAll({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
// (moveNode, reorderRoots, reorderChildren, cancelDeletion, and so on).
// The collection loop below classifies children via `_order.contains`;
// inside a [runBatch] the order still reflects state at batch entry,
// so a child whose visibility was changed by an earlier in-batch
// mutation would be misclassified, omitted from nodesToShow (pops in
// at full extent, never joins the bulk group). Same pattern as
// [insert] / [expand] / [collapse].
_ensureVisibleOrder();
// Collect all nodes to expand, nodes to show, and nodes currently exiting
final nodesToExpand = <TKey>[];
final nodesToShow = <TKey>[];
final nodesToReverseExit = <TKey>[];
// Scratch for the expansion-gated flatten harvest below.
final flattenScratch = <TKey>[];
// Iterative DFS. Depth is recomputed per-visit via [_depthOfKey]
// (matching the original recursive implementation) so we do not
// need to carry depth along in a parallel stack.
final stack = <TKey>[];
for (final rootId in _roots) {
stack.add(rootId);
}
while (stack.isNotEmpty) {
final key = stack.removeLast();
if (_isPendingDeletion(key)) {
continue;
}
final children = _childListOf(key);
if (children == null || children.isEmpty) {
continue;
}
final depth = _depthOfKey(key);
final withinDepthLimit = maxDepth == null || depth < maxDepth;
if (withinDepthLimit && !_isExpandedKey(key)) {
nodesToExpand.add(key);
for (final childId in children) {
if (!_order.contains(childId)) {
// Harvest the child plus everything reachable through
// already-expanded interior nodes: a hidden node B whose own
// expansion flag stayed true through an ancestor collapse
// becomes visible together with its children when [key]
// expands. The DFS below only harvests at nodes it flips
// (already-expanded children are never re-pushed as
// nodesToExpand, and depth-limited nodes are not descended
// into), so without this flatten those revealed descendants
// would render at full extent from frame one while everything
// around them animates, expand(key:) on the identical
// structure animates the whole revealed subtree.
flattenScratch.clear();
_flattenSubtreeInto(childId, flattenScratch, includeRoot: true);
for (final k in flattenScratch) {
// Preserve the child-level invariants: skip nodes already
// in the order (e.g. still animating an exit under this
// collapsed ancestor) and pending-deletion nodes.
if (!_order.contains(k) && !_isPendingDeletion(k)) {
nodesToShow.add(k);
}
}
}
}
}
// Harvest exiting children only when this parent is POST-FLIP
// expanded, which by induction from the roots means the children are
// post-flip visible: the DFS pushes children only when the parent is
// withinDepthLimit, and such a parent is either already expanded or
// joins nodesToExpand. A child harvested under a parent that stays
// collapsed would be un-pended below, grow back to full extent, and
// stay in the visible order (the terminal handlers never remove a
// completed enter).
final postFlipExpanded = withinDepthLimit || _isExpandedKey(key);
if (postFlipExpanded) {
for (final childId in children) {
// Check standalone exiting
final animation = _standaloneAt(childId);
if (animation != null && animation.type == AnimationType.exiting) {
if (!_isPendingDeletion(childId)) {
nodesToReverseExit.add(childId);
}
}
// Check operation group exiting (pendingRemoval)
final opGroupKey = _operationGroupOf(childId);
if (opGroupKey != null) {
final opGroup = _opGroupAt(opGroupKey);
if (opGroup != null && opGroup.pendingRemoval.contains(childId)) {
if (!_isPendingDeletion(childId)) {
nodesToReverseExit.add(childId);
}
}
}
}
}
// Only descend into children if within depth limit.
if (withinDepthLimit) {
for (final childId in children) {
stack.add(childId);
}
}
}
if (nodesToExpand.isEmpty && nodesToReverseExit.isEmpty) {
return;
}
// Batch update expansion states. Skip per-call ancestors-expanded
// propagation, we rebuild it wholesale below in O(N).
for (final key in nodesToExpand) {
_setExpandedKey(key, true, propagate: false);
_recordExpansionChange(key, wasExpanded: false);
}
_rebuildAllAncestorsExpanded();
// Rebuild visible order from scratch (more efficient for bulk operations)
_markVisibleOrderDirty();
// Flush the deferred rebuild NOW. The bulk-member registration loops
// below gate on `_order.contains(key)` to skip children that aren't
// structurally visible; inside a [runBatch], `_markVisibleOrderDirty`
// sets only a dirty flag and `_order` remains pre-mutation. Without
// this flush, every newly-visible child fails the `contains` check
// and never gets added to the bulk group, so the bulk animation runs
// empty and the children appear at full extent in one frame.
// Outside a batch this is a no-op (the mark already triggered the
// rebuild synchronously); inside a batch it forces consumption now,
// and any subsequent in-batch mutation can re-mark dirty as needed.
_ensureVisibleOrder();
// The harvest gate above makes this exact, not conservative: every
// harvested reverse-exit key is post-flip visible.
assert(nodesToReverseExit.every(_ancestorsExpandedFast));
// Start animations for newly visible nodes and reverse exiting animations
if (animate) {
// Reverse collapsing operation groups. Snapshot before iterating: a
// group's controller already at upperBound would fire `completed`
// synchronously inside `forward()`, 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.isNotEmpty) {
_partitionByPostFlipVisibility(
group.members.keys,
_partitionVisibleScratch,
_partitionHiddenScratch,
);
if (_partitionVisibleScratch.isEmpty) {
// No member is post-flip visible: leave the group pended and
// reversing. Its dismissed handler removes the members via
// _ancestorsExpandedFast; un-pending here would grow them
// back to full extent under a collapsed chain.
continue;
}
opGroupReversed = true;
// Capture the curved value BEFORE any write: the displacement
// reads it, and the rebase below re-anchors against it.
final preReversalCurvedValue = group.curvedValue;
// Displace the post-flip HIDDEN members to standalone exits
// BEFORE the pendingRemoval.clear() (so disposeIfEmpty stays
// hard-blocked by the non-empty pending set during the
// detaches) and BEFORE the rebase and reset (the capture inside
// _startStandaloneExitAnimation reads the endpoints the rebase
// overwrites and the curved value the reset moves).
for (final key in _partitionHiddenScratch) {
_startStandaloneExitAnimation(
key,
family: TreeAnimationFamily.expandCollapse,
);
}
group.pendingRemoval.clear();
// Path-1 rebase (expand Path 1) over the SURVIVORS: capture
// each member's CURRENT painted extent under the OLD envelope,
// re-anchor start to it, target full, and reset the controller
// so computeExtent at value 0 reproduces exactly that extent.
// Re-targeting alone pops by (full - oldTarget) * curvedValue.
//
// `?? defaultExtent` is deliberate and REPLACES the
// `?? _unknownExtent` that stood here. It is the same fallback
// the read path applies, so an unmeasured member's captured
// extent is its true painted one; and the sentinel must not be
// WRITTEN here, because computeExtent ignores both endpoints
// under it and the reset below would drop the row to zero in a
// single frame. An INCOMING sentinel is a different thing (the
// old collapseAll reversal never touched targetExtent, so one
// could arrive here); the capture absorbs it either way,
// returning full * preReversalCurvedValue, the member's true
// painted extent, so the sentinel leaves the record here.
for (final member in group.members.entries) {
final full = _fullExtentOf(member.key) ?? defaultExtent;
member.value.startExtent = member.value.computeExtent(
preReversalCurvedValue,
full,
);
member.value.targetExtent = full;
// A natural reference, not a capture: cleared unconditionally
// for the reason stated at expand Path 1.
member.value.targetIsCaptured = false;
}
_anim.opGroups.runWithGroupDetached(entry.key, (g) {
g.controller.value = 0.0;
});
group.controller.forward();
}
}
_opGroupSnapshot.clear();
if (opGroupReversed) _bumpAnimGen();
// Check if there's a collapsing bulk animation we can reverse
if (_activeBulkGroup != null &&
_activeBulkGroup!.pendingRemoval.isNotEmpty) {
// Partition the pending set by post-flip visibility: only a
// post-flip-visible member may un-pend and grow back; a member
// whose chain stays collapsed is displaced to a standalone exit
// so it finishes shrinking away instead of becoming a permanent
// hidden-parent row (the exit's finalizer removes it from the
// order via _ancestorsExpandedFast).
_partitionByPostFlipVisibility(
_activeBulkGroup!.pendingRemoval,
_partitionVisibleScratch,
_partitionHiddenScratch,
);
for (final key in _partitionHiddenScratch) {
// Captures full * bulk.value and removes the member from both
// bulk sets.
_startStandaloneExitAnimation(
key,
family: TreeAnimationFamily.expandCollapse,
);
}
final anyVisible = _partitionVisibleScratch.isNotEmpty;
for (final key in _partitionVisibleScratch) {
_removeBulkPending(key);
}
// Both sub-cases share one joiner policy: reversing exits and
// genuinely new rows ride standalone enters, never the mid-flight
// group. The partition only decides whether forward() runs.
_routeShownNodesToStandaloneEnters(nodesToReverseExit, nodesToShow);
if (anyVisible) {
// Reverse the controller direction
_activeBulkGroup!.controller.forward();
}
_bumpBulkGen();
} else if (_activeBulkGroup != null &&
_activeBulkGroup!.members.isNotEmpty) {
// Continuation: a bulk expand is already mid-flight (members
// present, nothing pending removal). Keep the group, existing
// members continue from their current extent. Creating a fresh
// group here would dispose the in-flight one and pop every
// half-expanded member to full extent in a single frame.
_routeShownNodesToStandaloneEnters(nodesToReverseExit, nodesToShow);
_bumpBulkGen();
} else {
// Create fresh group via the BulkAnimator (auto-disposes any
// prior). All listener wiring happens inside createGroup.
_anim.bulk.createGroup(
_animationStyle.expandCollapse.duration,
_animationStyle.expandCollapse.curve,
);
_bumpBulkGen(); // invalidate bulk + broad generation caches
// Reverse standalone exit animations smoothly
for (final key in nodesToReverseExit) {
if (!_hasOperationGroup(key)) {
_startStandaloneEnterAnimation(
key,
family: TreeAnimationFamily.expandCollapse,
);
}
}
// Add new nodes to the bulk group (skip if already in an operation
// group)
for (final key in nodesToShow) {
if (_order.contains(key) && !_hasOperationGroup(key)) {
_addBulkMember(key);
}
}
// Start expanding (value 0 -> 1)
_activeBulkGroup!.controller.forward();
_bumpBulkGen();
}
} else {
// Remove animations if not animating
for (final key in nodesToReverseExit) {
_removeAnimation(key);
}
}
// Bulk expansion touches many ancestors' expansion state + every
// previously-collapsed node now flips its chevron. Enumerating the
// affected set precisely is error-prone; fall back to a full refresh.
_notifyStructural();
}