大晚上的先上個圖片震撼一下你們的心靈。git
文本中帶有圖片/表情,在如今的app中,是及其常見的事情,可是在Flutter當中,這是個缺失的功能。github
就向上圖同樣,產品和UI設計是不可能放過個人。因此Extended Text就在這個天時地利人和的狀況下誕生了。canvas
花了些時間把Text的源碼都看一遍,很天然的看到了最後用Canvas在畫字,其實Flutter 的widget只是一個數據的殼,最終仍是都會落實在Canvas上面。那麼咱們不是就能夠在這個Canvas上面畫咱們像要的圖片了嗎?緩存
答案固然是能夠的,接下來,咱們把源碼Copy出來,魔改吧!!markdown
首先想到的是,這個圖片,確定也要佔用文字的位置,那麼我是否是能夠畫個透明的文字,而後在這個文字的位置上畫圖呢?網絡
先百度了一下(感謝RealRichText提供的思路),\u200B 字符表明 ZERO WIDTH SPACE,就是寬帶爲0的空白,我拿TextPainter試了下,確實是這樣,layout出來的Width老是0,無論fontSize是多少,固然高度會隨fontSize變化。結合TextStyle裏面的letterSpacing,這樣咱們就能控制這個圖片文字的寬度了。app
/// 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;
複製代碼
接下來,又是用TextPainter,計算出來26 fontSize的\u200B的高度爲30DP, 這樣咱們就知道怎麼把圖片文字的高度轉爲了文字的fontSize了。。async
//[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;
}
複製代碼
圖片文字那麼必然要有圖片了,那麼咱們就提供個ImageProvider來裝載圖片,由於作過extended image,這部分不要太熟悉了,對image不瞭解的同窗能夠去看看 這個 全能的Imageide
固然我沒有忘記給你們準備網絡圖片緩存的ImageProvider,以及清除它們的方法clearExtendedTextDiskCachedImagessvg
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
複製代碼
須要注意的是,由於ImageSpan無法獲取到BuildContext,因此咱們須要在Extended text build的時候,把ImageProvider 所須要的ImageConfiguration準備好
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);
}
});
}
複製代碼
接下來就要到核心繪畫文字的類裏面去了ExtendedRenderParagraph 在Paint方法中,在畫字以前咱們來處理這個圖片(反正文字是透明的,並且0的width,只是有個與先後文字的距離(圖片的寬)),在繪畫圖片的時候,我把畫布移動到offset的地方,就是整個文字開始繪畫的點,方便後面計算的繪畫
void paint(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 should be (0,0)
final Rect rect = Offset(0.0, 0.0) & size;
_paintSpecialTextChildren(<TextSpan>[text], canvas, rect);
canvas.restore();
}
複製代碼
在_paintSpecialTextChildren中,循環找尋ImageSpan. 注意使用getOffsetForCaret方法,咱們來判斷這個TextSpan是否已是文本溢出了。
Offset topLeftOffset = getOffsetForCaret(
TextPosition(offset: textOffset),
rect,
);
//skip invalid or overflow
if (topLeftOffset == null ||
(textOffset != 0 && topLeftOffset == Offset.zero)) {
return;
}
複製代碼
textOffset起始爲0,當跳過一個TextSpan,咱們加上該TextSpan的offset,而後繼續查找
textOffset += ts.toPlainText().length;
複製代碼
若是是一個ImageSpan,首先由於這個\u200B 沒有寬度,而寬度是咱們設置的letterSpacing,因此這個圖片繪畫的地方應該要向前移動width / 2.0
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 should be 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();
}
}
});
}
}
複製代碼
ImageSpan的paint方法,若是圖片還沒加載,那麼咱們須要resolveImage而且監聽回調,在回調的時候,若是是一個同步的回調,那麼這個時候Canvas應該不沒有被dispose掉,那麼咱們就直接畫上。不然判斷owner,而且設置markNeedsPaint,讓整個Text再次繪畫。
上面就是怎麼在文本中加入一個圖片,然而產品可不是那麼好對付的,產品說,那個圖片給我加個圓角,加個Border,加個加載效果,給弄成圓形的,巴拉巴拉...說累了,你就直接按照下面的圖來作吧。
看到這樣的需求,個人表情爲
不過其實掌握了Canvas的一些技巧以後,這點事情難不倒我,加上2個回調,在繪畫圖片以前和以後,作你想要作的任何事情。
///you can paint your placeholder or clip
///any thing you want
final BeforePaintImage beforePaintImage;
///you can paint border,shadow etc
final AfterPaintImage afterPaintImage;
複製代碼
好比說在圖片加載以後來個loading 佔位,你能夠這樣作
ImageSpan(CachedNetworkImage(imageTestUrls.first), beforePaintImage:
(Canvas canvas, Rect rect, ImageSpan imageSpan) {
bool hasPlaceholder = drawPlaceholder(canvas, rect, imageSpan);
if (!hasPlaceholder) {
clearRect(rect, canvas);
}
return false;
},
複製代碼
畫個背景,畫個字,so easy
bool drawPlaceholder(Canvas canvas, Rect rect, ImageSpan imageSpan) {
bool hasPlaceholder = 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.clear
canvas.saveLayer(rect, Paint());
canvas.drawRect(rect, Paint()..blendMode = BlendMode.clear);
canvas.restore();
}
複製代碼
其餘效果請參見 自定義圖片
最後放上 Github Extended_Text,若是你有什麼不明白的地方,請告訴我,歡迎加入Flutter Candies,一塊兒生產可愛的Flutter 小糖果(QQ羣:181398081)
Extended Text的功能遠遠不僅這些,將在下面的幾篇文章中慢慢道來。