Flutter 支持图片显示和自定义图片效果的文本

extended text 相关文章

大晚上的先上个图片震撼一下大家的心灵。

image

文本中带有图片/表情,在现在的app中,是及其常见的事情,但是在Flutter当中,这是个缺失的功能。

就向上图一样,产品和UI设计是不可能放过我的。所以Extended_Text就在这个天时地利人和的情况下诞生了。

花了些时间把Text的源码都看一遍,很自然的看到了最后用Canvas在画字,其实Flutter 的widget只是一个数据的壳,最终还是都会落实在Canvas上面。那么我们不是就可以在这个Canvas上面画我们像要的图片了吗?

答案当然是可以的,接下来,我们把源码Copy出来,魔改吧!!

image

首先想到的是,这个图片,肯定也要占用文字的位置,那么我是不是可以画个透明的文字,然后在这个文字的位置上画图呢?

先百度了一下(感谢RealRichText提供的思路),\u200B 字符代表 ZERO WIDTH SPACE,就是宽带为0的空白,我拿TextPainter试了下,确实是这样,layout出来的Width总是0,不管fontSize是多少,当然高度会随fontSize变化。结合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,
这样我们就知道怎么把图片文字的高度转为了文字的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

当然我没有忘记给大家准备网络图片缓存的把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中,循环找寻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,加个加载效果,给弄成圆形的,巴拉巴拉...说累了,你就直接按照下面的图来做吧。

image

看到这样的需求,我的表情为


image

不过其实掌握了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的功能远远不只这些,将在下面的几篇文章中慢慢道来。

image
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容