Created
October 17, 2022 07:33
-
-
Save matthew-carroll/5ecf8d00ce2840b3ab525d7d5d82fa6c to your computer and use it in GitHub Desktop.
flutter_text_clipping_bug
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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