Skip to content

Instantly share code, notes, and snippets.

@matthew-carroll
Created October 17, 2022 07:33
Show Gist options
  • Save matthew-carroll/5ecf8d00ce2840b3ab525d7d5d82fa6c to your computer and use it in GitHub Desktop.
Save matthew-carroll/5ecf8d00ce2840b3ab525d7d5d82fa6c to your computer and use it in GitHub Desktop.
flutter_text_clipping_bug
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
/// This app reproduces a text painting bug in which some characters, and some
/// descenders are cut off.
///
/// This repro captures an actual state of paragraph style, text style, and canvas
/// scaling. We're painting text in our app based on a document's specification of
/// characters and spacing between them. In general, this is working fine. However
/// there are numerous places where we see minor clipping, like we do with this example.
///
/// You'll notice that the font size is set to 1.0 and then the canvas is scaled up 24x,
/// resulting in an effective font size of 24. We do this because it matches the semantics
/// of the document. The document gives us the font size, and the painting transform.
void main() {
runApp(
const MaterialApp(
home: _TextClippingBug(),
),
);
}
class _TextClippingBug extends StatelessWidget {
const _TextClippingBug({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: CustomPaint(
painter: _ClippedTextPainter(
paragraphStyle: ui.ParagraphStyle(
textAlign: TextAlign.left,
fontWeight: FontWeight.w400,
fontStyle: FontStyle.normal,
fontSize: 1.0,
),
textStyle: ui.TextStyle(
color: const Color(0xff000000),
letterSpacing: -0.015,
wordSpacing: -0.02,
),
textAndSpaces: [
"mental ",
0.5,
"models ",
0.6,
"of ",
0.5,
"how ",
0.5,
"to ",
0.5,
"learn ",
0.5,
"effectively",
70,
". ",
0.5,
"Over",
17,
"all",
" ",
0.6,
"the ",
0.5,
"r",
17,
"esear",
17,
"ch ",
0.5,
"discussed ",
0.5,
"in ",
0.5,
"this ",
0.5,
"Review ",
0.5,
"has",
-15,
],
),
),
),
);
}
}
class _ClippedTextPainter extends CustomPainter {
const _ClippedTextPainter({
required this.textAndSpaces,
required this.paragraphStyle,
required this.textStyle,
});
final List<dynamic> textAndSpaces;
final ui.ParagraphStyle paragraphStyle;
final ui.TextStyle textStyle;
@override
void paint(Canvas canvas, Size size) {
final builder = ui.ParagraphBuilder(paragraphStyle);
print("Painting text");
print(" - text and spaces: $textAndSpaces");
print("");
print(" - style:");
print(textStyle.toString());
print("");
print(" - text builder:");
builder.pushStyle(textStyle);
final debugStringBuffer = StringBuffer();
for (final arg in textAndSpaces) {
if (arg is String) {
print(" - string: '$arg'");
builder.addText(arg);
debugStringBuffer.write(arg);
} else {
const fontSize = 1.0;
const horizontalScalePercent = 1.0;
final spacer = ((-arg / 1000) * fontSize) * horizontalScalePercent;
print(" - placeholder: $spacer px wide");
builder.addPlaceholder(spacer, 1, PlaceholderAlignment.bottom);
debugStringBuffer.write(" ");
}
}
print("");
final paragraph = builder.build()..layout(const ui.ParagraphConstraints(width: double.infinity));
canvas.save();
canvas.scale(24);
canvas.translate(-paragraph.maxIntrinsicWidth / 2, 0);
canvas.drawParagraph(paragraph, Offset(0, -paragraph.alphabeticBaseline));
canvas.restore();
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment