computeStickyHeaders method

Set<TKey> computeStickyHeaders({
  1. required double scrollOffset,
  2. required double overlap,
  3. required List<TKey> visibleNodes,
  4. required Float64List nodeOffsetsByNid,
  5. required Float64List nodeExtentsByNid,
  6. required FindFirstVisibleIndex findFirstVisibleIndex,
  7. double freshOffsetAt(
    1. int visibleIndex
    )?,
})

Computes the sticky set for this layout, populating headers and the per-nid lookup, and returns the candidate keys so the caller can force-create any that are outside the cache region.

ONE probe per call, and the caller repeats it ONLY when it actually force-created a row: that is the sole way the inputs, meaning measured extents and the offsets derived from them, can change between two probes within one layout. In the common case the sticky candidate is already inside the cache region and mounted, nothing is force-created, and a second walk would be identical work over identical inputs. See RenderSliverTree.performLayout. freshOffsetAt must be supplied whenever the caller's per-nid arrays are NOT the live offset authority (the render layer's bulk-only fast path, where off-cache slots go stale while bulk cumulatives move every frame); the probe then reads candidate offsets and subtree bottoms through it instead of the arrays.

Implementation

Set<TKey> computeStickyHeaders({
  required double scrollOffset,
  required double overlap,
  required List<TKey> visibleNodes,
  required Float64List nodeOffsetsByNid,
  required Float64List nodeExtentsByNid,
  required FindFirstVisibleIndex findFirstVisibleIndex,
  double Function(int visibleIndex)? freshOffsetAt,
}) {
  // Null out prior-layout sticky entries before recomputing. Iterate
  // [_writtenStickyNids] (the nids we wrote LAST frame) instead of
  // resolving keys back to nids: a key freed since the last layout by an
  // immediate-purge removal yields `noNid`, which would leave the
  // stale entry to leak stickiness onto whichever fresh key recycles
  // the nid. The nid handle is stable until reallocation, so clearing
  // through it is correct even when the original occupant is gone.
  for (int i = 0; i < _writtenStickyNidsLen; i++) {
    final nid = _writtenStickyNids[i];
    if (nid >= 0 && nid < _stickyByNid.length) {
      _stickyByNid[nid] = null;
    }
  }
  _writtenStickyNidsLen = 0;
  _stickyHeaders.clear();

  // Allocated only when the tree actually has sticky headers; the probe
  // returns immediately for `maxStickyDepth == 0`.
  final candidates = <TKey>{};
  double? parentPinnedY;
  _forEachStickyCandidate(
    scrollOffset: scrollOffset,
    overlap: overlap,
    visibleNodes: visibleNodes,
    nodeOffsetsByNid: nodeOffsetsByNid,
    nodeExtentsByNid: nodeExtentsByNid,
    findFirstVisibleIndex: findFirstVisibleIndex,
    freshOffsetAt: freshOffsetAt,
    onCandidate: (candidateId, pinnedY, extent, stackTop) {
      // Deeper headers can slide behind parent, but must never go above
      // parent TOP.
      if (parentPinnedY != null) {
        pinnedY = math.max(parentPinnedY!, pinnedY);
        if (pinnedY + extent <= stackTop) return false;
      }

      final indent = _controller.getIndent(candidateId);
      final info = StickyHeaderInfo<TKey>(
        nodeId: candidateId,
        pinnedY: pinnedY,
        extent: extent,
        indent: indent,
      );
      _stickyHeaders.add(info);
      final nid = _controller.nidOf(candidateId);
      _stickyByNid[nid] = info;
      if (_writtenStickyNidsLen == _writtenStickyNids.length) {
        final grown = Int32List(_writtenStickyNids.length * 2);
        grown.setRange(0, _writtenStickyNidsLen, _writtenStickyNids);
        _writtenStickyNids = grown;
      }
      _writtenStickyNids[_writtenStickyNidsLen++] = nid;

      parentPinnedY = pinnedY;
      candidates.add(candidateId);
      return true;
    },
  );
  return candidates;
}