paint method
Implementation
@override
String paint() {
final maxWidth = size.width.round();
final viewportHeight = size.height.round();
final separatorBreaks = _separatorBreaks(separator);
final itemCount = children.length;
final hasSelection =
controller is WidgetScrollController &&
(controller as WidgetScrollController).hasSelection;
if (itemCount == 0 || viewportHeight <= 0) {
_invalidateVisiblePaintCache();
_resetVisibleHitCache();
_resetRepaintFlag();
return '';
}
final offset = controller.offset.clamp(0, controller.maxOffset);
if (!hasSelection &&
_canUseVisiblePaintCache(
offset: offset,
viewportHeight: viewportHeight,
maxWidth: maxWidth,
itemCount: itemCount,
separatorBreaks: separatorBreaks,
isVariableHeight: variableHeight,
)) {
final cached = _cachedVisibleContent!;
_resetRepaintFlag();
return cached;
}
if (variableHeight) {
final baseEstimate = math
.max(1, estimatedItemExtent ?? itemExtent)
.toInt();
final estimate = _resolveAdaptiveEstimate(baseEstimate);
_syncCache(itemCount, separatorBreaks, estimate);
({
String visible,
bool heightsChanged,
int anchorIndex,
int anchorOffsetInItem,
})
buildVisibleForOffset(int target) {
_resetVisibleHitCache();
final prefix = _prefixHeights(itemCount, separatorBreaks, estimate);
var startIndex = _findStartIndex(prefix, target).clamp(0, itemCount);
var offsetInItem = target - prefix[startIndex];
if (startIndex >= itemCount) {
startIndex = itemCount - 1;
offsetInItem = 0;
}
final requiredLines = offsetInItem + viewportHeight;
final buffer = StringBuffer();
var lineCount = 0;
var measuredHeightsChanged = false;
_lastPaintOffset = target;
_lastPaintViewportHeight = viewportHeight;
_lastOffsetInItem = offsetInItem;
for (
var i = startIndex;
i < itemCount && lineCount < requiredLines;
i++
) {
final itemStart = lineCount;
final resolved = _resolveChildPaint(index: i, maxWidth: maxWidth);
final text = resolved.text;
final measured = resolved.measured;
if (_measuredHeights[i] != measured) {
_measuredHeights[i] = measured;
measuredHeightsChanged = true;
_invalidateMeasurements();
}
buffer.write(text);
lineCount += measured;
final itemEnd = lineCount;
if (itemEnd > offsetInItem && itemStart < requiredLines) {
_lastVisibleHits.add(
_VisibleItemHit(
index: i,
bufferStart: itemStart,
bufferEnd: itemEnd,
),
);
}
if (i < itemCount - 1 && separator.isNotEmpty) {
buffer.write(separator);
lineCount += separatorBreaks;
}
}
final content = buffer.toString();
return (
visible: _sliceLines(content, offsetInItem, viewportHeight),
heightsChanged: measuredHeightsChanged,
anchorIndex: startIndex,
anchorOffsetInItem: offsetInItem,
);
}
var workingOffset = offset;
var rendered = buildVisibleForOffset(workingOffset);
if (rendered.heightsChanged) {
_traceScroll(
'virtual_list.measurements_changed '
'zone=$zoneId items=$itemCount estimate=$estimate '
'offset=$workingOffset max=${controller.maxOffset}',
);
final contentHeight = _estimatedContentHeight(
itemCount,
separatorBreaks,
estimate,
);
_setContentExtent(contentHeight);
// Stabilize viewport anchor when measured heights change: keep the
// same first visible item and intra-item line, and move absolute
// offset to match the new prefix sum. This prevents apparent jumps
// backward/forward while estimates converge during scrolling.
final anchorOffset = _offsetForAnchor(
itemCount: itemCount,
separatorBreaks: separatorBreaks,
estimate: estimate,
anchorIndex: rendered.anchorIndex,
anchorOffsetInItem: rendered.anchorOffsetInItem,
).clamp(0, controller.maxOffset);
final currentOffset = controller.offset.clamp(0, controller.maxOffset);
final suppressAnchorAdjust =
controller is WidgetScrollController &&
(controller as WidgetScrollController).thumbDragActive;
if (anchorOffset != currentOffset && !suppressAnchorAdjust) {
_traceScroll(
'virtual_list.anchor_adjust '
'zone=$zoneId from=$currentOffset to=$anchorOffset '
'anchorIndex=${rendered.anchorIndex} '
'anchorInItem=${rendered.anchorOffsetInItem} '
'contentHeight=$contentHeight max=${controller.maxOffset}',
);
controller.jumpTo(anchorOffset);
} else if (anchorOffset != currentOffset && suppressAnchorAdjust) {
_traceScroll(
'virtual_list.anchor_skip '
'zone=$zoneId from=$currentOffset target=$anchorOffset '
'anchorIndex=${rendered.anchorIndex} '
'anchorInItem=${rendered.anchorOffsetInItem} '
'contentHeight=$contentHeight max=${controller.maxOffset}',
);
}
final nextOffset = controller.offset.clamp(0, controller.maxOffset);
if (nextOffset != workingOffset) {
_traceScroll(
'virtual_list.offset_resolved '
'zone=$zoneId $workingOffset->$nextOffset max=${controller.maxOffset}',
);
_resetRepaintFlag();
workingOffset = nextOffset;
rendered = buildVisibleForOffset(workingOffset);
}
}
_storeVisiblePaintCache(
visible: _applySelectionIfNeeded(rendered.visible, workingOffset),
offset: workingOffset,
viewportHeight: viewportHeight,
maxWidth: maxWidth,
itemCount: itemCount,
separatorBreaks: separatorBreaks,
isVariableHeight: true,
);
_resetRepaintFlag();
return _cachedVisibleContent!;
}
_resetVisibleHitCache();
final itemHeight = math.max(1, itemExtent).toInt();
final stride = itemHeight + separatorBreaks;
final startIndex = stride > 0 ? offset ~/ stride : 0;
final offsetInStride = stride > 0 ? offset % stride : 0;
final requiredLines = offsetInStride + viewportHeight;
final buffer = StringBuffer();
var lineCount = 0;
for (var i = startIndex; i < itemCount && lineCount < requiredLines; i++) {
final resolved = _resolveChildPaint(index: i, maxWidth: maxWidth);
final text = resolved.text;
buffer.write(text);
lineCount += resolved.measured;
if (i < itemCount - 1 && separator.isNotEmpty) {
buffer.write(separator);
lineCount += separatorBreaks;
}
}
final visible = _applySelectionIfNeeded(
_sliceLines(buffer.toString(), offsetInStride, viewportHeight),
offset,
);
_storeVisiblePaintCache(
visible: visible,
offset: offset,
viewportHeight: viewportHeight,
maxWidth: maxWidth,
itemCount: itemCount,
separatorBreaks: separatorBreaks,
isVariableHeight: false,
);
_resetRepaintFlag();
return visible;
}