extended textRelated articles
- Flutter RichText supports image display and custom image effects
- Flutter RichText supports custom text overflow effects
- Flutter RichText supports custom text backgrounds
- Flutter RichText supports special text effects
- Flutter RichText supports text selection
The first picture of the evening shocked everyone’s mind.
Text with images/expressions is a very common thing in current apps, but this is a missing feature in Flutter.
Just like above, product and UI design can’t get away with me. So Extended Text was born out of the right place at the right time.
I took some time to read all the source code of Text. It was natural to see that the Text was drawn on Canvas at last. In fact, the widget of Flutter is just a data shell and will be implemented on Canvas eventually. So can’t we draw the picture we want on this Canvas?
The answer is, of course, next, we put the source Copy out, magic change it!!
The first thing that comes to mind is that this image must also occupy the position of the text, so can I draw a transparent text, and then draw on the position of the text?
The \u200B character stands for ZERO WIDTH SPACE, which is a blank SPACE with a WIDTH of 0. I used TextPainter to try it. The layout WIDTH is always 0. No matter what fontSize is, of course the height will vary with fontSize. With the letterSpacing in TextStyle, we control the spacing of the text in the image.
/// The amount of space (in logical pixels) to add between each letter.
/// A negative value can be used to bring the letters closer.
final double letterSpacing;
Copy the code
Next,, use TextPainter again, calculate the height of 26 fontSize \u200B is 30DP, so we know how to convert the height of the image text to the text fontSize.
//[imageSpanTransparentPlaceholder] width is zero,
///so that we can define letterSpacing as Image Span width
const String imageSpanTransparentPlaceholder = "\u200B";
///transparentPlaceholder is transparent text
//fontsize id define image height
//size = 30.0/26.0 * fontSize
//final double size = 30.0;
///fontSize 26 and text height =30.0
// Final double fontSize = 26.0;
double dpToFontSize(double dp) {
return dp / 30.0 * 26.0;
}
Copy the code
We’ll provide an ImageProvider to load the image. Since we’ve done extended Image, you should not be too familiar with this part. If you don’t know anything about image, you can check out this all-powerful image
For you, of course, I didn’t forget ImageProvider network image cache, as well as the method to clear them clearExtendedTextDiskCachedImages
CachedNetworkImage(this.url,
{this.scale = 1.0.this.headers,
this.cache: false.this.retries = 3.this.timeLimit,
this.timeRetry = const Duration(milliseconds: 100)})
: assert(url ! =null),
assert(scale ! =null);
/// Clear the disk cache directory then return if it succeed.
/// <param name="duration">timespan to compute whether file has expired or not</param>
Future<bool> clearExtendedTextDiskCachedImages({Duration duration}) async
Copy the code
Note that since ImageSpan doesn’t get BuildContext, we need to have the ImageConfiguration required by the ImageProvider in place during the Extended Text build
void _createImageConfiguration(List<TextSpan> textSpan, BuildContext context) {
textSpan.forEach((ts) {
if (ts is ImageSpan) {
ts.createImageConfiguration(context);
} else if(ts.children ! =null) { _createImageConfiguration(ts.children, context); }}); }Copy the code
Then we go to the core drawing text class, ExtendedRenderParagraph. In Paint, before we Paint the text, we process the image (the text is transparent, and the width is 0, but there is a distance from the text). I moved the canvas to offset, which is the point where the whole text starts to paint, for the convenience of calculating the painting later
voidpaint(PaintingContext context, Offset offset) { _paintSpecialText(context, offset); _paint (context, offset); }void _paintSpecialText(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas;
canvas.save();
///move to extended text
canvas.translate(offset.dx, offset.dy);
///we have move the canvas, so rect top left shouldBe (0, 0)
final Rect rect = Offset(0.0.0.0) & size;
_paintSpecialTextChildren(<TextSpan>[text], canvas, rect);
canvas.restore();
}
Copy the code
In _paintSpecialTextChildren, loop for ImageSpan. Notice that we use the getOffsetForCaret method to determine if this TextSpan is already text overrun.
Offset topLeftOffset = getOffsetForCaret(
TextPosition(offset: textOffset),
rect,
);
//skip invalid or overflow
if (topLeftOffset == null|| (textOffset ! =0 && topLeftOffset == Offset.zero)) {
return;
}
Copy the code
The textOffset starts at 0, and when we skip a TextSpan, we add the offset of that TextSpan and keep looking
textOffset += ts.toPlainText().length;
Copy the code
If it is an ImageSpan, first of all because the \u200B has no width and the width is our letterSpacing, the image should be painted with width / 2.0 forward
if (ts is ImageSpan) {
///imageSpanTransparentPlaceholder \u200B has no width, and we define image width by
///use letterSpacing,so the actual top-left offset of image shouldBe subtract letterSpacing (width) / 2.0
Offset imageSpanOffset = topLeftOffset - Offset(ts.width / 2.0.0.0);
if(! ts.paint(canvas, imageSpanOffset)) {//image not ready
ts.resolveImage(
listener: (ImageInfo imageInfo, bool synchronousCall) {
if (synchronousCall)
ts.paint(canvas, imageSpanOffset);
else {
if (owner == null| |! owner.debugDoingPaint) { markNeedsPaint(); }}}); }}Copy the code
In ImageSpan paint, if the image has not been loaded, we need the resolveImage and listen for the callback. During the callback, if it is a synchronous callback, the Canvas should not be disposed and we should just paint it. Otherwise judge the owner and set markNeedsPaint so that the entire Text is drawn again.
Add a rounded corner, add a Border, add a loading effect, make it round, blah blah blah… When you’re tired of talking, you can just follow the picture below.
Seeing such a need, my expression is
This is not a problem for me after I’ve mastered some Canvas techniques, plus 2 callbacks, do whatever you want before and after drawing the image.
///you can paint your placeholder or clip
///any thing you want
final BeforePaintImage beforePaintImage;
///you can paint border,shadow etc
final AfterPaintImage afterPaintImage;
Copy the code
For example, you can do loading after the image is loaded
ImageSpan(CachedNetworkImage(imageTestUrls.first), beforePaintImage:
(Canvas canvas, Rect rect, ImageSpan imageSpan) {
bool hasPlaceholder = drawPlaceholder(canvas, rect, imageSpan);
if(! hasPlaceholder) { clearRect(rect, canvas); }return false;
},
Copy the code
Draw a background, draw a word, so easy
bool drawPlaceholder(Canvas canvas, Rect rect, ImageSpan imageSpan) {
boolhasPlaceholder = imageSpan.imageSpanResolver.imageInfo? .image ==null;
if(hasPlaceholder) { canvas.drawRect(rect, Paint().. color = Colors.grey);var textPainter = TextPainter(
text: TextSpan(text: "loading", style: TextStyle(fontSize: 10.0)),
textAlign: TextAlign.center,
textScaleFactor: 1,
textDirection: TextDirection.ltr,
maxLines: 1)
..layout(maxWidth: rect.width);
textPainter.paint(
canvas,
Offset(rect.left + (rect.width - textPainter.width) / 2.0,
rect.top + (rect.height - textPainter.height) / 2.0));
}
return hasPlaceholder;
}
void clearRect(Rect rect, Canvas canvas) {
///if don't save layer
///BlendMode.clear will show black
///maybe this is bug for blendMode.clearcanvas.saveLayer(rect, Paint()); canvas.drawRect(rect, Paint().. blendMode = BlendMode.clear); canvas.restore(); }Copy the code
For other effects, see Custom images
Let me know if you don’t understand it. Welcome to join Flutter Candies. Let’s make lovely Flutter Candies.
Extended Text does much more than that, which will be covered in the next few articles.