There is no Flutter API for getting the exact bounds of the text. Flutter: finding the exact bounds of text covers this. That said, I have a solution based on the same discussion.
The approach is to paint a character (a capital 'I' in my case) to a canvas and then scan the image pixels looking for the edge of the character. I count the rows of pixels between the character and image edge and use that to set the padding on the colored block. My solution is a little more involved because I have two Text widgets within a Column and each Text is a different size.
Note: I wouldn't advise this solution unless you really care about a precise alignment with the edge of the character.
The layout code:
IntrinsicHeight(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
FutureBuilder<TopBottomPadding>(
future: _calcPadding(
TextSpan(
text: "I", style: helloTextStyle),
TextSpan(
text: "I", style: everyoneTextStyle),
mediaQueryData.textScaleFactor),
builder: (BuildContext context, tuple) {
return Padding(
padding: EdgeInsets.only(
top: tuple.data.top,
bottom: tuple.data.bottom,
),
child: Container(
decoration: BoxDecoration(
border: Border(
left: BorderSide(
width: 16.0, color: Colors.red),
),
),
),
);
}),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("HELLO", style: helloTextStyle),
Text("EVERYONE", style: everyoneTextStyle),
],
),
],
),
)
Generating the image involves an async call and so I've enlisted the FutureBuilder widget.
Future<TopBottomPadding> _calcPadding(final TextSpan topSpan,
final TextSpan bottomSpan, final double textScaleFactor) async {
final topPadding = await _calcTopPadding(topSpan, textScaleFactor);
final bottomPadding = await _calcBottomPadding(bottomSpan, textScaleFactor);
return TopBottomPadding(topPadding, bottomPadding);
}
Future<double> _calcTopPadding(TextSpan span, double textScaleFactor) async {
final int bytesPerPixel = 4;
final imageData =
await _getImageByteData(span, ImageByteFormat.rawRgba, textScaleFactor);
final Size imageSize = imageData.size;
final ByteData byteData = imageData.byteData;
final numRows =
(byteData.lengthInBytes / (bytesPerPixel * imageSize.width)).round();
int foundRow;
/// Scan each pixel from top to bottom keeping track of the row
for (int row = 0; row < numRows && foundRow == null; row++) {
final int rowLength = bytesPerPixel * imageSize.width.round();
final int startRowByteIndex = row * rowLength;
/// Only looking at first byte of each pixel is good enough
for (int byteArrayIndex = startRowByteIndex;
byteArrayIndex < row * rowLength + rowLength;
byteArrayIndex += bytesPerPixel) {
final int byteValue = byteData.getUint8(byteArrayIndex);
/// The background is white so look for a non-white pixel.
if (foundRow == null && byteValue != 0xff) {
foundRow = row;
break;
}
}
}
final double result = foundRow == null ? 0 : foundRow.toDouble();
return result;
}
Future<double> _calcBottomPadding(
final TextSpan span, final textScaleFactor) async {
final int bytesPerPixel = 4;
final imageData =
await _getImageByteData(span, ImageByteFormat.rawRgba, textScaleFactor);
final Size imageSize = imageData.size;
final ByteData byteData = imageData.byteData;
final numRows =
(byteData.lengthInBytes / (bytesPerPixel * imageSize.width)).round();
int foundRow;
/// Scan each pixel from bottom to top keeping track of the row
for (int row = numRows - 1; row >= 0 && foundRow == null; row--) {
final int rowLength = bytesPerPixel * imageSize.width.round();
final int startRowByteIndex = row * rowLength;
/// Only looking at first byte of each pixel is good enough
for (int byteArrayIndex = startRowByteIndex;
byteArrayIndex < row * rowLength + rowLength;
byteArrayIndex += bytesPerPixel) {
final int byteValue = byteData.getUint8(byteArrayIndex);
/// The background is white so look for a non-white pixel.
if (foundRow == null && byteValue != 0xff) {
foundRow = row;
break;
}
}
}
final double foundRowIndex = foundRow == null ? 0 : foundRow.toDouble();
final int heightAsZeroBasedIndex = imageSize.height.round() - 1;
final double paddingValue = heightAsZeroBasedIndex - foundRowIndex;
return paddingValue;
}
Future<ImageData> _getImageByteData(final TextSpan span,
final ImageByteFormat byteFormat, final double textScaleFactor) async {
final painter = TextPainter(
text: span,
textDirection: TextDirection.ltr,
textScaleFactor: textScaleFactor);
painter.layout();
final imageData = ImageData();
imageData.size = Size(painter.width, painter.height);
final recorder = PictureRecorder();
final screen = Offset.zero & imageData.size;
final canvas = Canvas(recorder);
drawBackground(canvas, screen);
painter.paint(canvas, Offset.zero);
final picture = recorder.endRecording();
final image =
await picture.toImage(screen.width.round(), screen.height.round());
final ByteData byteData = await image.toByteData(format: byteFormat);
imageData.byteData = byteData;
return imageData;
}
void drawBackground(final Canvas canvas, final Rect screen) {
canvas.drawRect(
screen,
Paint()
..color = Colors.white
..style = PaintingStyle.fill);
}
class TopBottomPadding {
double top;
double bottom;
TopBottomPadding(this.top, this.bottom);
}
class ImageData {
ByteData byteData;
Size size;
}
This solution works for any screen density, font size, or text scale factor.