Flutter RichText支持圖片顯示和自定義圖片效果

extended text 相關文章

大晚上的先上個圖片震撼一下你們的心靈。git

文本中帶有圖片/表情,在如今的app中,是及其常見的事情,可是在Flutter當中,這是個缺失的功能。github

就向上圖同樣,產品和UI設計是不可能放過個人。因此Extended Text就在這個天時地利人和的狀況下誕生了。canvas

pub package

花了些時間把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的功能遠遠不僅這些,將在下面的幾篇文章中慢慢道來。

相關文章
相關標籤/搜索