measureNextPage method
Measures the next page of text and returns its data without rasterizing. This is useful for previewing content or getting layout information.
Implementation
Future<PageData?> measureNextPage() async {
if (_remainingText.isEmpty) return null;
final List<_LineData> lines = [];
String textToLayout = _remainingText;
double currentY = layout.startY;
final double estimatedLineHeight = layout.fontSize * 1.4;
while (textToLayout.isNotEmpty && currentY < layout.height) {
// Get the available width for a line at the current Y position.
var lineLayout = layout.getLineLayout(currentY, estimatedLineHeight);
if (lineLayout == null) {
// We have moved outside the drawable area, so this page is done.
break;
}
// Use a Paragraph to measure the text for the current line.
final paragraphBuilder = ui.ParagraphBuilder(ui.ParagraphStyle(
textAlign: layout.textAlign,
fontFamily: layout.fontFamily,
fontSize: layout.fontSize.toDouble(),
textDirection: ui.TextDirection.ltr,
));
paragraphBuilder.addText(textToLayout);
final paragraph = paragraphBuilder.build();
paragraph.layout(ui.ParagraphConstraints(width: lineLayout.width.toDouble()));
// Get the metrics for the first line that fits within the constraints.
final lineMetrics = paragraph.computeLineMetrics();
if (lineMetrics.isEmpty) break; // No more text fits.
final firstLine = lineMetrics.first;
final double actualLineHeight = firstLine.height;
// Final check to ensure this line actually fits vertically.
if (currentY + actualLineHeight > layout.height) {
break;
}
// Re-check the layout with the actual height for better accuracy, especially for circles.
var finalLineLayout = layout.getLineLayout(currentY, actualLineHeight) ?? lineLayout;
// Get the character range for the first line. This is the most reliable way
// to determine where Flutter decided to break the line.
final lineBreak = paragraph.getLineBoundary(const ui.TextPosition(offset: 0));
int endIndex = lineBreak.end;
if (endIndex == 0 && textToLayout.isNotEmpty) {
// Failsafe for cases where not even one character fits. Consume one to prevent loops.
endIndex = 1;
}
String lineText = textToLayout.substring(0, endIndex);
// Refine word wrapping: if the line breaks in the middle of a word,
// try to break at the previous space instead.
if (endIndex < textToLayout.length) {
final nextChar = textToLayout[endIndex];
if (lineText.isNotEmpty && !_isWhitespace(nextChar) && !_isWhitespace(lineText[lineText.length - 1])) {
int lastSpace = lineText.trimRight().lastIndexOf(' ');
if (lastSpace != -1) {
// Re-measure endIndex to the character after the space.
endIndex = textToLayout.substring(0, lastSpace).length + 1;
lineText = textToLayout.substring(0, endIndex);
}
}
}
// Only add the line if it contains non-whitespace characters.
final trimmedLine = lineText.trim();
if (trimmedLine.isNotEmpty) {
lines.add(_LineData(
text: trimmedLine,
width: finalLineLayout.width,
xOffset: finalLineLayout.xOffset,
yOffset: currentY.toInt(),
lineHeight: actualLineHeight.toInt(),
));
}
// Advance Y position and update remaining text.
currentY += actualLineHeight;
textToLayout = textToLayout.substring(endIndex).trimLeft();
}
if (lines.isEmpty && _remainingText.isNotEmpty) {
// This can happen if the first word is too long to fit on any line.
// To prevent an infinite loop, we consume the text that was attempted.
_remainingText = textToLayout;
return null;
}
_remainingText = textToLayout;
return PageData._(lines: lines, layout: layout);
}