Flutter 支持圖片顯示和自定義圖片效果的文本

extended text 相關(guān)文章

大晚上的先上個圖片震撼一下大家的心靈拦英。

image

文本中帶有圖片/表情,在現(xiàn)在的app中壹士,是及其常見的事情螟蒸,但是在Flutter當中抄瑟,這是個缺失的功能。

就向上圖一樣贺纲,產(chǎn)品和UI設(shè)計是不可能放過我的。所以Extended_Text就在這個天時地利人和的情況下誕生了。

花了些時間把Text的源碼都看一遍墨状,很自然的看到了最后用Canvas在畫字,其實Flutter 的widget只是一個數(shù)據(jù)的殼,最終還是都會落實在Canvas上面感耙。那么我們不是就可以在這個Canvas上面畫我們像要的圖片了嗎?

答案當然是可以的,接下來只酥,我們把源碼Copy出來裂允,魔改吧!!

image

首先想到的是,這個圖片,肯定也要占用文字的位置隙赁,那么我是不是可以畫個透明的文字,然后在這個文字的位置上畫圖呢弟灼?

先百度了一下(感謝RealRichText提供的思路),\u200B 字符代表 ZERO WIDTH SPACE,就是寬帶為0的空白欧穴,我拿TextPainter試了下笑诅,確實是這樣同蜻,layout出來的Width總是0,不管fontSize是多少默责,當然高度會隨fontSize變化。結(jié)合TextStyle里面的letterSpacing,這樣我們就能控制這個圖片文字的寬度了芦鳍。

  /// 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,
這樣我們就知道怎么把圖片文字的高度轉(zhuǎn)為了文字的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;
}

圖片文字那么必然要有圖片了,那么我們就提供個ImageProvider來裝載圖片谈宛,因為做過extended image次哈,這部分不要太熟悉了,對image不了解的同學可以去看看 這個 全能的Image

當然我沒有忘記給大家準備網(wǎng)絡(luò)圖片緩存的把ImageProvider,以及清除它們的方法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

需要注意的是吆录,因為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中济榨,循環(huán)找尋ImageSpan.
注意使用getOffsetForCaret方法樱拴,我們來判斷這個TextSpan是否已經(jīng)是文本溢出了。

 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铅檩,然后繼續(xù)查找

textOffset += ts.toPlainText().length;

如果是一個ImageSpan,首先因為這個\u200B 沒有寬度兔沃,而寬度是我們設(shè)置的letterSpacing殿雪,所以這個圖片繪畫的地方應(yīng)該要向前移動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并且監(jiān)聽回調(diào)缺菌,在回調(diào)的時候葫辐,如果是一個同步的回調(diào),那么這個時候Canvas應(yīng)該不沒有被dispose掉伴郁,那么我們就直接畫上另患。否則判斷owner,并且設(shè)置markNeedsPaint蛾绎,讓整個Text再次繪畫昆箕。

上面就是怎么在文本中加入一個圖片,然而產(chǎn)品可不是那么好對付的租冠,產(chǎn)品說鹏倘,那個圖片給我加個圓角,加個Border顽爹,加個加載效果纤泵,給弄成圓形的,巴拉巴拉...說累了镜粤,你就直接按照下面的圖來做吧捏题。

image

看到這樣的需求,我的表情為


image

不過其實掌握了Canvas的一些技巧之后肉渴,這點事情難不倒我,加上2個回調(diào)公荧,在繪畫圖片之前和之后,做你想要做的任何事情同规。

  ///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,一起生產(chǎn)可愛的Flutter 小糖果(QQ群:181398081)

Extended Text的功能遠遠不只這些程腹,將在下面的幾篇文章中慢慢道來匣吊。

image
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市寸潦,隨后出現(xiàn)的幾起案子缀去,更是在濱河造成了極大的恐慌,老刑警劉巖甸祭,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異褥影,居然都是意外死亡池户,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門凡怎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來校焦,“玉大人,你說我怎么就攤上這事统倒≌洌” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵房匆,是天一觀的道長耸成。 經(jīng)常有香客問我,道長浴鸿,這世上最難降的妖魔是什么井氢? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮岳链,結(jié)果婚禮上花竞,老公的妹妹穿的比我還像新娘。我一直安慰自己掸哑,他們只是感情好约急,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著苗分,像睡著了一般厌蔽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上摔癣,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天躺枕,我揣著相機與錄音服猪,去河邊找鬼。 笑死拐云,一個胖子當著我的面吹牛罢猪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播叉瘩,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼膳帕,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了薇缅?” 一聲冷哼從身側(cè)響起危彩,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎泳桦,沒想到半個月后汤徽,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體灸撰,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡完疫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了芳誓。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片兆沙。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖库正,靈堂內(nèi)的尸體忽然破棺而出褥符,到底是詐尸還是另有隱情趟大,我是刑警寧澤逊朽,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站岛蚤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏她紫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一朴乖、第九天 我趴在偏房一處隱蔽的房頂上張望买羞。 院中可真熱鬧畜普,春花似錦吃挑、人聲如沸舶衬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽众雷。三九已至砾省,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間狠鸳,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工铅祸, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓红淡,卻偏偏與公主長得像荤傲,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子颈渊,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容