1. 场景阐明

最近遇到一个小问题,这儿把问题模型简化,记载一下处理方式,也算是一个小留念。先说一下场景,如下所示:

已知字符串 src
匹配段列表:matches

算法遇记 | 字符串段拆插问题 - 富文本


这样,在 Flutter 中能够经过对 matches遍历,形成富文本段,进行展现,效果如下:

算法遇记 | 字符串段拆插问题 - 富文本

TextSpan formSpan() {
  List<List<int>> matches = [
    [1, 2], [5, 8], [14, 15]
  ];
  String src = "toly 1994,hello!";
  List<InlineSpan> span = [];
  int cursor = 0;
  for (int i = 0; i < matches.length; i++) {
    List<int> match = matches[i];
    // 非匹配段
    String noMatchStr = src.substring(cursor, match[0]);
    span.add(TextSpan(text:noMatchStr , style: style0));
    // 匹配段
    String matchStr = src.substring(match[0], match[1]+1);
    span.add(TextSpan(text: matchStr, style: style1,));
    cursor = match[1]+1;
  }
  if (cursor != src.length - 1) {
    span.add(TextSpan(text: src.substring(cursor), style: style0));
  }
  return TextSpan(children: span);
}

2. 要处理的需求

现在有个需求,给定槽点列表 slots,在 坚持原有匹配效果 的前提下,在每个槽点对应的索引处,刺进该槽点的索引值,如下所示:

算法遇记 | 字符串段拆插问题 - 富文本

如下,是刺进后的效果,其间本来的高亮款式坚持不变,且在指定方位处额定刺进了文字。这时候 有用怪 难抑心中疑问,发出灵魂呼喊:这有什么用呢?

算法遇记 | 字符串段拆插问题 - 富文本


如下所示,如果在定点刺进的东西不是文字,而是其他组件,比方 FlutterLogo 。就完成了在不影响原有高亮匹配情况下,在指定槽位刺进其他组件的能力:

算法遇记 | 字符串段拆插问题 - 富文本

说一个最直接的使用场景,如下代码高亮行号的刺进,便是使用这种手段。不影响原有富文本,在定点刺进指定组件。

代码高亮 + 行号 代码高亮 + 行号
算法遇记 | 字符串段拆插问题 - 富文本
算法遇记 | 字符串段拆插问题 - 富文本

3. 完成思路

这个问题的实质是根据 slots 点,对已字符段进行切割。就像一个拼接手术:首要找到方位,然后剪开,把刺进段放在两片之间,再黏在一同:

算法遇记 | 字符串段拆插问题 - 富文本


因为槽点能够在恣意方位,所以关于每段来说,操作都是一致的。这样关于每段字符,能够封装一个通用办法来处理。如下,界说 insertSlotWithBoundary 办法,传入每段的起止索引。第一步,应该校验当时段中是否存在槽点。如下左图所示,该段无槽点,就不需求进行什么处理:

算法遇记 | 字符串段拆插问题 - 富文本

这儿界说 slotCursor 记载槽点数组的游标,它会跟着每次槽点被处理,而自加。所以某段在处理时,经过 slots[slotCursor] 能够得到当时待入槽点方位。如下所示,当 slotCursor 长度大于大于总槽位时,阐明已经刺进结束,不需求关注槽点了;或者 待入槽点方位 要比 end 还大,阐明当时段没有槽点:

int slotCursor = 0;
insertSlotWithBoundary(int start, int end, TextStyle style) {
  if (slotCursor >= slots.length || slots[slotCursor] > end) {
    // 阐明当时段没有槽点,无需处理
    span.add(TextSpan(
      text: src.substring(start, end),
      style: style,
    ));
    return;
  }
  // TODO 槽点处理
}

在某段中,或许存在 n 个槽点,把段切割为 n+1 段。结合 slotCursor 游标和 end 值,能够经过 while 循环进行遍历处理:

算法遇记 | 字符串段拆插问题 - 富文本

在进入循环时,将 slotCursor++,需求留意截取的结尾需求额定处理一下。若干槽位已经结束,或下一槽位大于 end ,阐明 下一槽点不再当时段。 将截取的结尾设为 end :

insertSlotWithBoundary(int start, int end, TextStyle style) {
  // 同上,略...
  // 有槽点,切割插槽
  String matchStr = src.substring(start, slots[slotCursor]);
  span.add(TextSpan(text: matchStr, style: style));
  while (slots[slotCursor] < end) {
    int slotPosition = slots[slotCursor];
    slotCursor++;
    int currentEndPosition = 0;
    if (slotCursor == slots.length || slots[slotCursor] > end) {
      // 阐明插槽结束
      // 阐明下一槽点不再当时段
      currentEndPosition = end;
    } else {
      currentEndPosition = slots[slotCursor];
    }
    // 刺进槽点组件:
    span.add(const WidgetSpan(child: FlutterLogo()));
    String matchStr = src.substring(slotPosition, currentEndPosition);
    span.add(TextSpan(
      text: matchStr,
      style: style,
    ));
    if (slotCursor >= slots.length) break;
  }
}

到这儿,处理就完成了,尽管代码量比较少,可是其间需求考虑的点挺多的。包括校验条件、循环流程、游标处理等。在完成期间也走了不少弯路,试错花了不少时间,在调试中逐步处理问题。本以为我完成不了代码高亮的行号显示的,但在耐心和剖析中还是写出来了,过程可谓是痛快的。

现在总算能够在 Flutter 中代码展现或者文本展现时加上行号了,仅以此文留念这份自主处理问题的的愉悦感。下面是完整的 formSpan 办法,感兴趣的能够自己试一下:

TextSpan formSpan() {
  List<List<int>> matches = [[1, 2], [5, 8], [14, 15]];
  List<int> slots = [0, 2, 6, 8, 11, 13];
  String src = "toly 1994,hello!";
  List<InlineSpan> span = [];
  int cursor = 0;
  int slotCursor = 0;
  insertSlotWithBoundary(int start, int end, TextStyle style) {
    if (slotCursor>=slots.length||slots[slotCursor] > end) {
      // 阐明当时段没有槽点,无需处理
      span.add(TextSpan(
        text: src.substring(start, end),
        style: style,
      ));
      return;
    }
    // 有槽点,切割插槽
    String matchStr = src.substring(start, slots[slotCursor]);
    span.add(TextSpan(text: matchStr, style: style));
    while (slots[slotCursor] < end) {
      int slotPosition = slots[slotCursor];
      slotCursor++;
      int currentEndPosition = 0;
      if (slotCursor == slots.length || slots[slotCursor] > end) {
        // 阐明插槽结束
        // 阐明下一槽点不再当时段
        currentEndPosition = end;
      } else {
        currentEndPosition = slots[slotCursor];
      }
      span.add(const WidgetSpan(child: FlutterLogo()));
      String matchStr2 = src.substring(slotPosition, currentEndPosition);
      span.add(TextSpan(
        text: matchStr2,
        style: style,
      ));
      if (slotCursor >= slots.length) break;
    }
  }
  for (int i = 0; i < matches.length; i++) {
    List<int> match = matches[i];
    insertSlotWithBoundary(cursor, match[0], style0);
    insertSlotWithBoundary(match[0], match[1] + 1, style1);
    cursor = match[1] + 1;
  }
  if (cursor != src.length - 1) {
    insertSlotWithBoundary(cursor,src.length, style0);
  }
  return TextSpan(children: span);
}