flutter-text

Flutter源码解析-text

2019-07-24  本文已影响0人  叶落清秋

说明

本文源码基于flutter 1.7.8
相对于旧版本而言,text进行了一定的改动,已经支持了图文混编
之前看一些大佬魔改源码的时候,内部实现是添加了一个ImageSpan,在ImageSpan内部绘制图片。而新版本改动有些大,新增了一个WidgetSpan和TextSpan共同继承自InlineSpan。
对比而言,官方这个策略更优一些,毕竟ImageSpan只能针对图片文件,而WidgetSpan针对的是一个Widget控件,内部的实现也全权交给这个widget自己。

使用

简单列举一下怎么使用的

class TextWithWidgetDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RichText(
      text: TextSpan(
        text: "你好",
        children: [
          WidgetSpan(child: Icon(Icons.ac_unit)),
          WidgetSpan(child: FlatButton(onPressed: (){}, child: Text("点我"))),
          TextSpan(text: "这是什么啦啦啦"),
          TextSpan(
            text: "层级复杂的",
            children: [
              TextSpan(
                text: "点我",
                style: TextStyle(color: Colors.red, fontSize: 16),
                recognizer: TapGestureRecognizer()..onTap = (){
                  print("点击事件");
                }
              ),
            ],
          ),
        ],
        style: Theme.of(context).textTheme.display1,
      ),
    );
  }
}

分析问题

为了防止一深入源码里就迷失了,我们带着下面2个问题去阅读源码

  1. text是如何实现图文混编的
  2. text是如何处理超出数据长度

开始分析

因为text内部使用的是RichText富文本控件,所以我们直接从RichText开始分析:

//RichText继承MultiChildRenderObjectWidget,内部可以有多个子控件
class RichText extends MultiChildRenderObjectWidget {
  RichText({
    Key key,
    @required this.text,
    this.textAlign = TextAlign.start,
    this.textDirection,
    this.softWrap = true,
    this.overflow = TextOverflow.clip,
    this.textScaleFactor = 1.0,
    this.maxLines,
    this.locale,
    this.strutStyle,
    this.textWidthBasis = TextWidthBasis.parent,
  }) : ...
        //这一步记住了,已经将属于WidgetSpan的widget列表传递给了父类
       super(key: key, children: _extractChildren(text));
       
  //主要目的是遍历Span,然后取出WidgetSpan内子控件列表
  static List<Widget> _extractChildren(InlineSpan span) {
    final List<Widget> result = <Widget>[];
    //调用InlineSpan的visitChildren方法,InlineSpan是一个抽象类,其中的这个方法是抽象方法
    span.visitChildren((InlineSpan span) {
      //判断Span是否为WidgetSpan,添加到列表中
      if (span is WidgetSpan) {
        result.add(span.child);
      }
      return true;
    });
    return result;
  }
}

新版本将RichText的父类改为MultiChildRenderObjectWidget,老版本是一个LeafRenderObjectWidget(想看旧版本的RichText,可以看下我之前的文章:https://www.jianshu.com/p/1012d79551bc
也就是说RichText将是一个布局控件,内部可以摆放多个子控件

InlineSpan的结构图

InlineSpan是一个抽象类,将会有WidgetSpan和TextSpan来实现visitChildren方法

  typedef InlineSpanVisitor = bool Function(InlineSpan span);
  //---  TextSpan  ---
  @override
  bool visitChildren(InlineSpanVisitor visitor) {
    //text就是自己传递的String字符串
    if (text != null) {
      //调用方法,如果返回false将不调用下面的部分
      if (!visitor(this))
        return false;
    }
    //上面的方法,visitor默认返回true,所以一定会执行到这
    if (children != null) {
      for (InlineSpan child in children) {
        //遍历下去
        if (!child.visitChildren(visitor))
          return false;
      }
    }
    return true;
  }

  //---  WidgetSpan  ---
  @override
  bool visitChildren(InlineSpanVisitor visitor) {
    //直接返回visitor,并将自己传递出去
    return visitor(this);
  }

到这里为止,RichText已经收集了所有的WidgetSpan中的子控件,用我们开始的例子来说的话,就是已经有了Icon和FlatButton
继续往下阅读

  //RichText类
  @override
  RenderParagraph createRenderObject(BuildContext context) {
    assert(textDirection != null || debugCheckHasDirectionality(context));
    return RenderParagraph(text,
      textAlign: textAlign,
      textDirection: textDirection ?? Directionality.of(context),
      softWrap: softWrap,
      overflow: overflow,
      textScaleFactor: textScaleFactor,
      maxLines: maxLines,
      strutStyle: strutStyle,
      textWidthBasis: textWidthBasis,
      locale: locale ?? Localizations.localeOf(context, nullOk: true),
    );
  }

这里创建了一个RenderParagraph,这个是绘制的核心,这里将会进行layout、paint和事件分发操作

1. layout 过程

我们逐一分析,先看layout过程

class RenderParagraph extends RenderBox
    with ContainerRenderObjectMixin<RenderBox, TextParentData>,
             RenderBoxContainerDefaultsMixin<RenderBox, TextParentData> {
  @override
  void performLayout() {
    //摆放子控件的位置
    _layoutChildren(constraints);
    //摆放文本的位置
    _layoutTextWithConstraints(constraints);
    _setParentData();
    //后面那部分主要是一些异常情况和越界裁剪处理
    ...
  }
}

1.1 摆放子控件的流程

  void _layoutChildren(BoxConstraints constraints) {
    if (childCount == 0) {
      return;
    }
    RenderBox child = firstChild;
    final List<PlaceholderDimensions> placeholderDimensions = List<PlaceholderDimensions>(childCount);
    int childIndex = 0;
    while (child != null) {
      //让child进行摆放一下,主要是为了获取到child的size
      child.layout(
        BoxConstraints(
          maxWidth: constraints.maxWidth,
        ),
        parentUsesSize: true
      );
      double baselineOffset;
      switch (_placeholderSpans[childIndex].alignment) {
        case ui.PlaceholderAlignment.baseline: {
          baselineOffset = child.getDistanceToBaseline(_placeholderSpans[childIndex].baseline);
          break;
        }
        default: {
          baselineOffset = null;
          break;
        }
      }
      //封装一下,_placeholderSpans是在构造函数中,和之前获取WidgetSpan一样,获取的是一个PlaceholderSpan列表,这里包含了对齐方式
      placeholderDimensions[childIndex] = PlaceholderDimensions(
        size: child.size,
        alignment: _placeholderSpans[childIndex].alignment,
        baseline: _placeholderSpans[childIndex].baseline,
        baselineOffset: baselineOffset,
      );
      child = childAfter(child);
      childIndex += 1;
    }
    //设置值,将_textPainter的 _placeholderDimensions 赋值(后面会用到这个)
    _textPainter.setPlaceholderDimensions(placeholderDimensions);
  }

关于firstChild是怎么获取到的,这个过程发送在build的过程,简单说明一下:
布局控件的mount方法 --(遍历子控件) -> 子孩子的mount方法 -> RenderObjectElement的attachRenderObject - > insertChildRenderObject -> ContainerRenderObjectMixin的insert -> _insertIntoChildList -> _firstChild赋值

void _insertIntoChildList(ChildType child, { ChildType after }) {
    final ParentDataType childParentData = child.parentData;
    _childCount += 1;
    if (after == null) {
      // 第1个子孩子
      childParentData.nextSibling = _firstChild;
      if (_firstChild != null) {
        final ParentDataType _firstChildParentData = _firstChild.parentData;
        _firstChildParentData.previousSibling = child;
      }
     //赋值
      _firstChild = child;
      _lastChild ??= child;
    } else {
      final ParentDataType afterParentData = after.parentData;
      if (afterParentData.nextSibling == null) {
        // 第2个子孩子
        childParentData.previousSibling = after;
        afterParentData.nextSibling = child;
        _lastChild = child;
      } else {
        //子孩子大于或等于3个则构建链表
        childParentData.nextSibling = afterParentData.nextSibling;
        childParentData.previousSibling = after;
        final ParentDataType childPreviousSiblingParentData = childParentData.previousSibling.parentData;
        final ParentDataType childNextSiblingParentData = childParentData.nextSibling.parentData;
        childPreviousSiblingParentData.nextSibling = child;
        childNextSiblingParentData.previousSibling = child;
        assert(afterParentData.nextSibling == child);
      }
    }
  }

1.2 摆放文本的位置流程

  void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) {
    final bool widthMatters = softWrap || overflow == TextOverflow.ellipsis;
    _textPainter.layout(minWidth: minWidth, maxWidth: widthMatters ? maxWidth : double.infinity);
  }

  void _layoutTextWithConstraints(BoxConstraints constraints) {
    _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
  }

即调用_textPainter.layout

  //---  TextPainter类  ---
  void layout({ double minWidth = 0.0, double maxWidth = double.infinity }) {
    if (!_needsLayout && minWidth == _lastMinWidth && maxWidth == _lastMaxWidth)
      return;
    _needsLayout = false;
    if (_paragraph == null) {
      final ui.ParagraphBuilder builder = ui.ParagraphBuilder(_createParagraphStyle());
      //这个是关键点,核心部分,用了我们上一步传递的_placeholderDimensions
      _text.build(builder, textScaleFactor: textScaleFactor, dimensions: _placeholderDimensions);
      _inlinePlaceholderScales = builder.placeholderScales;
      _paragraph = builder.build();
    }
    _lastMinWidth = minWidth;
    _lastMaxWidth = maxWidth;
    //进行一次摆放
    _paragraph.layout(ui.ParagraphConstraints(width: maxWidth));
    if (minWidth != maxWidth) {
      final double newWidth = maxIntrinsicWidth.clamp(minWidth, maxWidth);
      if (newWidth != width) {
        _paragraph.layout(ui.ParagraphConstraints(width: newWidth));
      }
    }
   //获取占位符的位置坐标盒子列表
    _inlinePlaceholderBoxes = _paragraph.getBoxesForPlaceholders();
  }

我之前的文章介绍过使用Paragraph可以绘制文本,这里TextPainter其实就是对Paragraph的一层封装
_text是什么呢?这个就是RenderParagraph构造函数中传递的InlineSpan text,也就是我们最原始使用的那个InlineSpan,也就是RichText的text值

  //---  TextSpan  ---
  @override
  void build(ui.ParagraphBuilder builder, { double textScaleFactor = 1.0, List<PlaceholderDimensions> dimensions }) {
    final bool hasStyle = style != null;
    if (hasStyle)
      builder.pushStyle(style.getTextStyle(textScaleFactor: textScaleFactor));
    if (text != null)
      //添加文本String数据,加了之后就能绘制文本
      builder.addText(text);
    if (children != null) {
      for (InlineSpan child in children) {
        //遍历children下去,重复同样的操作
        child.build(builder, textScaleFactor: textScaleFactor, dimensions: dimensions);
      }
    }
    if (hasStyle)
      builder.pop();
  }

  //---  WidgetSpan  ---
    @override
  void build(ui.ParagraphBuilder builder, { double textScaleFactor = 1.0, @required List<PlaceholderDimensions> dimensions }) {
    final bool hasStyle = style != null;
    if (hasStyle) {
      builder.pushStyle(style.getTextStyle(textScaleFactor: textScaleFactor));
    }
    //使用了我们自己摆放的控件信息,包含了此控件的大小、对齐方式和基线
    final PlaceholderDimensions currentDimensions = dimensions[builder.placeholderCount];
    //建立一个控件大小的占位区,占着位置
    builder.addPlaceholder(
      currentDimensions.size.width,
      currentDimensions.size.height,
      alignment,
      scale: textScaleFactor,
      baseline: currentDimensions.baseline,
      baselineOffset: currentDimensions.baselineOffset,
    );
    if (hasStyle) {
      builder.pop();
    }
  }

到了这里,控件使用同等大小的占位符给代替了,文本还是照常摆放

2. paint过程

@override
  void paint(PaintingContext context, Offset offset) {
    
    _layoutTextWithConstraints(constraints);

    if (_needsClipping) {
      final Rect bounds = offset & size;
      if (_overflowShader != null) {
        context.canvas.saveLayer(bounds, Paint());
      } else {
        context.canvas.save();
      }
      context.canvas.clipRect(bounds);
    }
    //绘制文本
    _textPainter.paint(context.canvas, offset);
    //绘制控件
    RenderBox child = firstChild;
    int childIndex = 0;
    while (child != null) {
      assert(childIndex < _textPainter.inlinePlaceholderBoxes.length);
      final TextParentData textParentData = child.parentData;

      final double scale = textParentData.scale;
      //让控件位移之前layout计算的距离,也就是移动到占位符的区域
      context.pushTransform(
        needsCompositing,
        offset + textParentData.offset,
        Matrix4.diagonal3Values(scale, scale, scale),
        (PaintingContext context, Offset offset) {
          context.paintChild(
            child,
            offset,
          );
        },
      );
      child = childAfter(child);
      childIndex += 1;
    }
    if (_needsClipping) {
      if (_overflowShader != null) {
        context.canvas.translate(offset.dx, offset.dy);
        final Paint paint = Paint()
          ..blendMode = BlendMode.modulate
          ..shader = _overflowShader;
        context.canvas.drawRect(Offset.zero & size, paint);
      }
      context.canvas.restore();
    }
  }

至此,控件是怎么绘制上去的:其实就是先占位,然后真实的控件再平移一段距离在绘制到这个点(至于是画布平移还是矩阵变换,感兴趣的可以自己去看看源码,这里就不深入了)

3. 事件分发

添加了子控件,子控件的事件需要自己去处理,文本的事件则自己处理(还是原来的处理方式)

  @override
  bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
    RenderBox child = firstChild;
    while (child != null) {
      final TextParentData textParentData = child.parentData;
      //配套对应,将子控件的触摸反馈点也进行平移
      final Matrix4 transform = Matrix4.translationValues(textParentData.offset.dx, textParentData.offset.dy, 0.0)
        ..scale(textParentData.scale, textParentData.scale, textParentData.scale);
      final bool isHit = result.addWithPaintTransform(
        transform: transform,
        position: position,
        hitTest: (BoxHitTestResult result, Offset transformed) {
          return child.hitTest(result, position: transformed);
        },
      );
      //如果点击事件发生在平移的widget控件上,则其自己处理事件,不调用自己的handleEvent
      if (isHit) {
        return true;
      }
      child = childAfter(child);
    }
    //默认情况下自己处理事件,调用自己的handleEvent
    return false;
  }

  //当hitTestChildren返回false时就会调用该方法
  @override
  bool hitTestSelf(Offset position) => true;

看看hitTestChildren和hitTestSelf何时被调用

  //---  proxy_box  ---
  @override
  bool hitTest(BoxHitTestResult result, { Offset position }) {
    bool hitTarget = false;
    if (size.contains(position)) {
      //因为hitTestSelf默认为true,也就是说无论如何都会去调用handleEvent
      hitTarget = hitTestChildren(result, position: position) || hitTestSelf(position);
      if (hitTarget || behavior == HitTestBehavior.translucent)
        //将自己加入列表中,主要起作用的是handleEvent
        result.add(BoxHitTestEntry(this, position));
    }
    return hitTarget;
  }

然后对文本事件进行事件分配

  @override
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
    assert(debugHandleEvent(event, entry));
    if (event is! PointerDownEvent)
      return;
    _layoutTextWithConstraints(constraints);
    //获取点击点的本地坐标(不是绝对坐标,这个坐标是相对父控件而言的)
    final Offset offset = entry.localPosition;
    //获得_textPainter上点击位置对于的索引坐标,占位符占一个单位
    final TextPosition position = _textPainter.getPositionForOffset(offset);
     //获取到坐标上对应的一个TextSpan
    final TextSpan span = _textPainter.text.getSpanForPosition(position);
    //如果存在recognizer,则使其加入到事件分发中
    span?.recognizer?.addPointer(event);
  }

对事件分发还不太了解的可以去看看我之前写的事件分发(https://www.jianshu.com/p/1012d79551bc)和GestureDetector分析(https://www.jianshu.com/p/dc4853c33562

接下来第二个问题,我文本超出长度了,为什么可以以...结束

enum TextOverflow {
  /// 裁剪超出的部分
  clip,

  ///超出的部分设置为透明
  fade,

  /// 使用...结束
  ellipsis,

  /// 超出的部分仍让它显示
  visible,
}

在构造函数中,可以看到赋值

_textPainter = TextPainter(
         text: text,
         textAlign: textAlign,
         textDirection: textDirection,
         textScaleFactor: textScaleFactor,
         maxLines: maxLines,
         //是否绘制...
         ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null,
         locale: locale,
         strutStyle: strutStyle,
         textWidthBasis: textWidthBasis,
       )

这个值传递到TextPainter中被使用

  ui.ParagraphStyle _createParagraphStyle([ TextDirection defaultTextDirection ]) {
    return _text.style?.getParagraphStyle(
      textAlign: textAlign,
      textDirection: textDirection ?? defaultTextDirection,
      textScaleFactor: textScaleFactor,
      maxLines: _maxLines,
      // 使用
      ellipsis: _ellipsis,
      locale: _locale,
      strutStyle: _strutStyle,
    ) ?? ui.ParagraphStyle(
      textAlign: textAlign,
      textDirection: textDirection ?? defaultTextDirection,
      //设置最大行数
      maxLines: maxLines,
      ellipsis: ellipsis,
      locale: locale,
    );
  }

从这里也可以得出,是否越界其实是由Paragraph说了算的

//这个决定一行可以画多宽
_paragraph.layout(ui.ParagraphConstraints(width: newWidth));

到这里也算简单的分析完了,可能会有疑惑为什么不再分析Paragraph,后面都是native代码了,也就是调用c中的skia库来绘制
别着急,接下来我们用个简单的例子来演示这个流程,加深点印象

模仿demo

//图文混编排版测试
class LayoutDemo extends MultiChildRenderObjectWidget{
  LayoutDemo({
    Key key,
    List<Widget> children,
  }): super(key: key , children: children);

  @override
  RenderLayout createRenderObject(BuildContext context) {
    return RenderLayout();
  }
}

class PageParentData extends ContainerBoxParentData<RenderBox> {}

class RenderLayout extends RenderBox with ContainerRenderObjectMixin<RenderBox, PageParentData>,RenderBoxContainerDefaultsMixin<RenderBox, PageParentData>{
  double textWidth = 100;
  double textFontSize = 12.0;
  ui.Paragraph paragraph;
  List<TextBox> _inlinePlaceholderBoxes;

  RenderLayout({List<RenderBox> children}){
    ui.ParagraphBuilder paragraphBuilder = ui.ParagraphBuilder(
      ui.ParagraphStyle(
        textAlign: TextAlign.start,
        fontSize: textFontSize,
        textDirection: TextDirection.ltr,
        maxLines: 1,
        ellipsis: "...", //超出用...代替
      ),
    )
      ..pushStyle(
        ui.TextStyle(
            color: Colors.black87, textBaseline: ui.TextBaseline.alphabetic),
      )
      ..addText("前面部分")
      ..addPlaceholder(50, 50, ui.PlaceholderAlignment.middle)
      ..addText("中间部分")
      ..addPlaceholder(50, 20, ui.PlaceholderAlignment.bottom)
      ..addText("尾巴");

    paragraph = paragraphBuilder.build()
      ..layout(ui.ParagraphConstraints(width: 300));

    //获得空白占位的盒子位置列表,即addPlaceholder部分(源码中使用的是textPainter.layout ,内部会调用这个)
    _inlinePlaceholderBoxes = paragraph.getBoxesForPlaceholders();
  }

  @override
  void setupParentData(RenderBox child) {
    if (child.parentData is! PageParentData)
      child.parentData = PageParentData();
  }


  @override
  void performLayout() {
    size = Size(300,300);
    double widthOffset = 0.0;
    double heightOffset = 0.0;
    RenderBox child = firstChild;
    int index = 0;
    while(child != null){
      print("index = $index    ${_inlinePlaceholderBoxes[index].left}");
      widthOffset = _inlinePlaceholderBoxes[index].left;
      heightOffset = _inlinePlaceholderBoxes[index].top;
      final PageParentData childParentData = child.parentData;
      child.layout(constraints.heightConstraints(), parentUsesSize: true);
      childParentData.offset = Offset(widthOffset, heightOffset);
      index++;
      child = childParentData.nextSibling;
    }
  }

  //当hitTestChildren返回false时就会调用该方法
  //hitTestSelf返回true代表自己能处理本事件,调用自己的handleEvent方法
  @override
  bool hitTestSelf(Offset position) => true;

  @override
  bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
    RenderBox child = firstChild;
    while (child != null) {
      final PageParentData textParentData = child.parentData;
      //配套对应,将触摸反馈点也进行平移
      final Matrix4 transform = Matrix4.translationValues(textParentData.offset.dx, textParentData.offset.dy, 0.0)
        ..scale(1.0, 1.0, 1.0);
      final bool isHit = result.addWithPaintTransform(
        transform: transform,
        position: position,
        hitTest: (BoxHitTestResult result, Offset transformed) {
          return child.hitTest(result, position: transformed);
        },
      );
      //如果点击事件发生在平移的widget控件上,则其自己处理事件,不调用自己的handleEvent
      if (isHit) {
        return true;
      }
      child = childAfter(child);
    }
    //默认情况下自己处理事件,调用自己的handleEvent
    return false;
  }

  @override
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
    assert(debugHandleEvent(event, entry));
    if (event is! PointerDownEvent)
      return;
    //默认的文本处理方式,当textSpan中设置了recognizer,即捕获事件
    //找到对应位置的span,然后让其捕获事件,span?.recognizer?.addPointer(event);
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    var canvas = context.canvas;
    double scale = 1.0;
    //绘制文字
    canvas.drawParagraph(paragraph, offset);

    RenderBox child = firstChild;
    while(child != null){
      final PageParentData childParentData = child.parentData;
      //位移控件,即将控件平移offset + childParentData.offset距离
      context.pushTransform(
        true,
        offset + childParentData.offset,
        Matrix4.diagonal3Values(scale, scale, scale),
            (PaintingContext context, Offset offset) {
          context.paintChild(
            child,
            offset,
          );
        },
      );
      child = childParentData.nextSibling;
    }
  }
}

使用:

LayoutDemo(
                children: <Widget>[
                Icon(Icons.ac_unit),
                Icon(Icons.link),
                ],
              )

效果图:


demo效果图

结尾

可能有时候需要折叠文本,那么怎么知道文本到了可以折叠的地方呢(这里也可以用于对小说的章节页数划分)

      //计算是否需要折叠
      TextPainter textPainter = TextPainter(textDirection: TextDirection.ltr);
      textPainter.text =
          TextSpan(text: widget.str, style: TextStyle(fontSize: 14,color: Colors.grey),);
      textPainter.layout(maxWidth: width);
      //获取占满这个区域的String的最后一个字符的index(第几个就返回几)
      int end = textPainter.getPositionForOffset(Offset(width, 50.0)).offset;
      //得到这个end也可以对字符串就行拆分

再说一个源码的错误:
在上面分析handleEvent的时候有这一段代码

final TextSpan span = _textPainter.text.getSpanForPosition(position);

在有用WidgetSpan的时候返回的 TextSpan是错误的

  InlineSpan getSpanForPosition(TextPosition position) {
    //单独封装了一个value,便于传值
    //只是简单的使用int i = 0的话,然后将i传递给函数,函数内修改i的值,后面的span使用的i值是不会变的仍是0,封装成对象则不一样,值是会变的
    final Accumulator offset = Accumulator();
    InlineSpan result;
    visitChildren((InlineSpan span) {
      //遍历所有子控件,找到对应索引点位置的span(这里主要找的是textspan)
      result = span.getSpanForPositionVisitor(position, offset);
      return result == null;
    });
    return result;
  }

getSpanForPositionVisitor到底做了什么?
举个例子:
比如这个格式的: "hi"+占位符+"你好"+"我在这"
当我们点击到占位符上的时候,返回的索引就是3(hi占2个,占位符占1个)
第一次循环是"hi"
offset的值为0,position的值为3,并不满足下面那个条件判断,offset的值变为2
第二次循环是占位符,直接返回null,offset的值还是2
第三次循环是"你好",正好满足if判断的第二个条件,所以返回的是"你好"的span
span不为null,循环终止,最终值为"你好"
但实际上我们我们点击的是占位符上的控件

  // ---  TextSpan  ---
  @override
  InlineSpan getSpanForPositionVisitor(TextPosition position, Accumulator offset) {
    if (text == null) {
      return null;
      }
    final TextAffinity affinity = position.affinity;
    final int targetOffset = position.offset;
    final int endOffset = offset.value + text.length;
    if (offset.value == targetOffset && affinity == TextAffinity.downstream ||
        offset.value < targetOffset && targetOffset < endOffset ||
        endOffset == targetOffset && affinity == TextAffinity.upstream) {
      return this;
    }
    //即相当于index会加一个当前text的长度,并且这是个对象,后面使用的offset值就是当前的
    offset.increment(text.length);
    return null;
  }

  // ---  WidgetSpan  ---
  @override
  InlineSpan getSpanForPositionVisitor(TextPosition position, Accumulator offset) {
    return null;
  }

修改起来也简单

  // ---  WidgetSpan  ---
  @override
  InlineSpan getSpanForPositionVisitor(TextPosition position, Accumulator offset) {
    //add 
    offset.increment(1);
    return null;
  }

尾巴

文中demo的github地址:https://github.com/leaf-fade/flutter_source/blob/master/lib/text/text.dart

上一篇下一篇

猜你喜欢

热点阅读