paint method

  1. @override
void paint(
  1. PaintingContext context,
  2. Offset offset
)
override

Paint this render object into the given context at the given offset.

Subclasses should override this method to provide a visual appearance for themselves. The render object's local coordinate system is axis-aligned with the coordinate system of the context's canvas and the render object's local origin (i.e, x=0 and y=0) is placed at the given offset in the context's canvas.

Do not call this function directly. If you wish to paint yourself, call markNeedsPaint instead to schedule a call to this function. If you wish to paint one of your children, call PaintingContext.paintChild on the given context.

When painting one of your children (via a paint child function on the given context), the current canvas held by the context might change because draw operations before and after painting children might need to be recorded on separate compositing layers.

Implementation

@override
void paint(PaintingContext context, Offset offset) {
  if (geometry == null || geometry!.paintExtent == 0) return;

  final scrollOffset = constraints.scrollOffset;
  final remainingPaintExtent = constraints.remainingPaintExtent;
  final visibleNodes = controller.visibleNodes;
  final orderNids = controller.orderNidsView;

  // Widen the paint iteration start by the active FLIP-slide overreach
  // so rows structurally before the viewport but painting INTO it (via
  // a positive slide delta) are not skipped. `_paintRow` already bails
  // on rows whose painted y lies past the viewport, so extra iterated
  // rows on the bottom edge are harmless. See the matching comment in
  // `performLayout` for why structural offsets alone aren't enough.
  final slideOverreach = controller.maxActiveSlideAbsDelta;
  final startIndex =
      _findFirstVisibleIndex(scrollOffset - slideOverreach);

  // Pass A: Paint non-sticky nodes. Rows with a non-zero slide delta are
  // deferred to a second sub-pass so they paint on top of static rows —
  // without this, an upward-moving row that hasn't yet crossed into its
  // final index slot would be covered by siblings sliding down past it.
  // Among sliding rows, sort by ascending |delta| so the row that moved
  // the most (typically the just-dropped row) paints last and lands on
  // top. Ties preserve natural iteration order.
  List<int>? slidingIndices;
  for (int i = startIndex; i < visibleNodes.length; i++) {
    final nid = orderNids[i];
    if (_sticky.isSticky(nid)) {
      continue;
    }

    final nodeId = visibleNodes[i];
    final child = getChildForNode(nodeId);
    if (child == null) continue;

    // Paint-only FLIP slide delta — read from the controller on every
    // frame so localToGlobal / semantics (which can resolve between
    // ticks) always see the current value.
    final slideDelta = controller.getSlideDeltaNid(nid);

    if (slideDelta != 0.0) {
      (slidingIndices ??= <int>[]).add(i);
      continue;
    }

    _paintRow(
      context: context,
      offset: offset,
      nid: nid,
      child: child,
      slideDelta: 0.0,
      scrollOffset: scrollOffset,
      remainingPaintExtent: remainingPaintExtent,
    );
  }

  if (slidingIndices != null) {
    slidingIndices.sort((a, b) {
      final da = controller.getSlideDeltaNid(orderNids[a]).abs();
      final db = controller.getSlideDeltaNid(orderNids[b]).abs();
      final cmp = da.compareTo(db);
      if (cmp != 0) return cmp;
      return a.compareTo(b);
    });
    for (final i in slidingIndices) {
      final nodeId = visibleNodes[i];
      final child = getChildForNode(nodeId);
      if (child == null) continue;
      _paintRow(
        context: context,
        offset: offset,
        nid: orderNids[i],
        child: child,
        slideDelta: controller.getSlideDeltaNid(orderNids[i]),
        scrollOffset: scrollOffset,
        remainingPaintExtent: remainingPaintExtent,
      );
    }
  }

  // Pass B: Paint sticky headers (deepest first so shallower paints on top).
  final paintExtent = geometry!.paintExtent;
  final stickyHeaders = _sticky.headers;
  for (int i = stickyHeaders.length - 1; i >= 0; i--) {
    final sticky = stickyHeaders[i];
    final child = getChildForNode(sticky.nodeId);
    if (child == null) continue;
    // Skip nodes currently animating out. Sticky recompute is throttled
    // during animations, so _stickyHeaders may still contain entries for
    // nodes that just entered pendingRemoval — painting them would leave
    // a ghost row until the next recompute tick.
    if (controller.isExiting(sticky.nodeId)) continue;

    // Don't paint a header that has been pushed entirely past the sliver's
    // paint region (e.g. by a tiny remainingPaintExtent near the bottom).
    if (sticky.pinnedY >= paintExtent) continue;

    // Clip to whichever is smaller: the header's natural extent, or the
    // remaining paint region. Without this clamp the header would spill
    // into the next sliver when pinnedY + extent > paintExtent.
    final clippedExtent = math.min(
      sticky.extent,
      paintExtent - sticky.pinnedY,
    );
    if (clippedExtent <= 0) continue;

    final paintOffset = offset + Offset(sticky.indent, sticky.pinnedY);
    context.pushClipRect(
      needsCompositing,
      paintOffset,
      Rect.fromLTWH(0, 0, child.size.width, clippedExtent),
      (context, offset) {
        context.paintChild(child, offset);
      },
    );
  }
}