flutter TextField@人或者#事件

废话少说,先直接看效果图

艾特.gif

之前在做原生安卓时候 实现过类似的功能,地址https://www.jianshu.com/p/5398df1c8f2c?v=1701076000905
近段时间转flutter ,也需要实现类似的需求,百度一下,发现这方面的文章很少,有的demo跑起来完全不是我需要的效果。
折腾了一两天,还是自己手动撸吧
这里仅仅演示了 添加文字块,没有演示删除效果,之前是模拟器 flutter的RawKeyboardListener无法监听到模拟器的按键事件,查了很久发现是 只能监听物理按键,不能监听屏幕按键,后面有小伙伴发现ios也监听不到,果断放弃RawKeyboardListener,换另一种思路来解决,目前在模拟器测试是ok的。

这个@功能整理思路很重要,这里我去分析了安卓版实现的原理,很多功能 flutter都是没法做到和原生一致的,都是换一种思路来实现。
不过flutter实现起来代码量很少,只有三个文件,大部分都是在FTextEditingController中实现,维护比较方便
flutter和原生最大的不同就是设置高亮颜色的方式,
原生是

editable.setSpan(new ForegroundColorSpan(color), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

flutter则是通过TextEditingController的buildTextSpan方法来重新拼接
比如说 “我是@张三啊实打实”,我想让“@张三”高亮
那么需要怎么拼接的?
第一种方式是 ,通过正则来比配 “@张三”,则分成三部分 :“我是”,“@张三”,“啊实打实”
也就是三个TextSpan拼接起来 :
[TextSpan(“我是”).TextSpan(“@张三”),TextSpan(“啊实打实”)]

第二种是indexOf(highlight, start)来寻找@张三的起始位置,因为@张三有可能是多个,
这里使用死循环来寻找,如下:

  void highlight(){
    int start = 0, end;
    int index;
    String text = "我是@张三啊实打实";
    String highlight = '@张三';
    while ((index = text.indexOf(highlight, start)) > -1) {
      end = index + highlight.length;
      // [index, end ]就是@张三的起始位置
      start = end; // 下一次从上次发现的位置开始往后寻找
    }
  }

我这边使用正则来比配,感觉没有那么绕脑,第二种方式也可以,看个人使用
以下是文件夹结构


image.png

选择@人 或者#事件,这里没有使用原生的多态来实现,感觉多态不容易看懂,选择艾特一般就两三个类型,多态感觉用在大型项目比较合适,

// 这里就是模拟页面跳转选择之后,插入文字块
  void addAit(){
    int id=10;
    String name="张三";
    // 第一个参数就是拼接成@张三 格式,建议后面加空格,原生安卓版不加会有问题,
    // 原因看安卓版有说明https://www.jianshu.com/p/5398df1c8f2c?v=1701076000905
    // 第二个参数就是 提交给后端的格式,可以自己自定义
    SpanEntity span=SpanEntity("@${name} ","{[@$name,$id]}",color: Colors.blue);
    _controller.insertSpan(span);
  }

  void addEvent(){
    int id=9999;
    String name="alan";
    SpanEntity span=SpanEntity("#${name} ","{[#$name,$id]}",color: Colors.orange);
    _controller.insertSpan(span);
  }


void add$Span(){
    int id=555;
    String name="公司";
    SpanEntity span=SpanEntity("\$${name} ","{[\$$name,$id]}",color: Colors.orange);
    _controller.insertSpan(span);
  }
// 如果使用$作为特殊字符开头,在Controller还要做一层处理不然,比配不到
String get rulerText {
    return spanList.map((e) => e.text.startsWith('\$') ? '\\${e.text}' : e.text).toSet().toList().join('|');
  }

不用多态就直接定义一个类就可以,其实原生也可以直接这样,省事,通俗易懂,只是懒得去修改原生的了


image.png

发现了一个bug,如下图所示

ait_bug.gif

只要第一个是文字块,然后又跑到最前面插入文字块,就会导致光标位置错乱
具体原因可以查看代码分析

// 分析:
    // 如果第一个文本就是一个文字块,比如span(0,6)
    // 此时光标移入0的位置,再次插入文字块span(0,4)
    // 就会变成 spanList=[span(0,6),span(0,4)],也就是文字块的起始位置冲突了
    // 也就是span(0,6)本来应该往后位移,但是两个位置冲突了,beforeTextChanged方法无法正常执行

之前的判断是,根据文字块的开始位置即start来设置Offset,也就是当我插入文字快,你的start大于当前插入的start就要设置偏移量,很明显spanList=[span(0,6),span(0,4)],这里没法做偏移量,因为start都是0

spanList.forEach((element) {
        if (element.start > start && count != 0) {
          element.setOffset(count);
        }
        print('11count: $count, start: ${element.start}, end: ${element.end}');
      });
      isInsertSpan = false;

解决:
1.给SpanEntity 增加一个id属性,默认按时间戳生成,这样可以在添加之后过滤掉本次添加span
思路:过滤掉本次添加span之后,只要start>=当前插入span的start(element.start >= start),都要做偏移量
过滤完之后,spanList=[span(0,6),span(0,4)]会就变成 spanList=[span(0,6)],那么span(0,6)就可以设置对应的偏移量
修改前

void beforeTextChanged() {
    int count = text.length - textSize;
    int end = selection.end;
    if (end < 0) {
      // 没有焦点的情况下直接返回,一般是event编辑的时候会出现
      textSize = text.length;
      return;
    }
    int start = end - count;
    if (isInsertSpan) {
      spanList.forEach((element) {
        if (element.start > start && count != 0) {
          element.setOffset(count);
        }
        print('11count: $count, start: ${element.start}, end: ${element.end}');
      });
      isInsertSpan = false;
    } else {
      spanList.forEach((element) {
        if (element.start >= start && count != 0) {
          element.setOffset(count);
        }
        print('22count: $count, start: ${element.start}, end: ${element.end}');
      });
    }
    // print('count: $count, start: $start, end: $end');
    textSize = text.length;
  }

修改后

 void beforeTextChanged() {
    int count = text.length - textSize;
    int end = selection.end;
    if (end < 0) {
      // 没有焦点的情况下直接返回,一般是event编辑的时候会出现
      textSize = text.length;
      return;
    }
    int start = end - count;
    List<SpanEntity> list = spanList;
    if (insertSpanCache!=null) {
      // 过滤掉本次插入的文字块
      list = list.where((el) => el.id!=insertSpanCache!.id).toList();
      insertSpanCache = null;
    }
    list.forEach((element) {
      if (element.start >= start && count != 0) {
        element.setOffset(count);
      }
    });
    // print('count: $count, start: $start, end: $end');
    textSize = text.length;
  }

2024-6-26发现一个可以去掉正则的写法,而且比正则更容易看懂和维护,关键在于spanList,

spanList包含了所有文字快的信息(主要是起始位置信息),那完全没有必要使用正则,
直接使用遍历spanList,然后拼接不是更好?
先看原本的代码

TextSpan buildTextSpan(
      {required BuildContext context,
      TextStyle? style,
      required bool withComposing}) {
    RegExp regex = RegExp(rulerText); // RegExp(r'@牛逼|#大事件');
    Iterable<RegExpMatch> matches = regex.allMatches(value.text);
    int lastEnd = 0;
    List<TextSpan> spans = [];
    for (RegExpMatch match in matches) {
      final matchText = match.group(0);
      // 前部分文本
      final preMatch = text.substring(lastEnd, match.start);
      if (preMatch.isNotEmpty) {
        spans.add(TextSpan(text: preMatch));
      }
      Color highLightColor =spanColor[matchText]?? Colors.red;
      // 目标文本
      spans.add(
          TextSpan(text: matchText, style: TextStyle(color: highLightColor)));

      lastEnd = match.end;
    }
    // 目标后面的文本
    final tail = text.substring(lastEnd);
    if (tail.isNotEmpty) {
      spans.add(TextSpan(text: tail));
    }

    return TextSpan(style: style, children: spans);
  }

修改之后

  @override
  TextSpan buildTextSpan(
      {required BuildContext context,
      TextStyle? style,
      required bool withComposing}) {
      return formatInputText(style);
  }
  // 这里直接使用spanList来遍历重新组合TextSpan ,省去使用正则 ,而且完全没有必要使用正则
 TextSpan formatInputText(TextStyle? style) {
    int lastEnd = 0;
    List<TextSpan> spans = [];
    for (SpanEntity span in spanList) {
      if (lastEnd <= text.length && span.start <= text.length) {
        final startText = text.substring(lastEnd, span.start);
        if (startText.isNotEmpty) {
          spans.add(TextSpan(text: startText));
        }
        String matchText = span.text;
        Color highLightColor = span.color ?? Colors.red;
        // 目标文本
        spans.add(
            TextSpan(text: matchText, style: TextStyle(color: highLightColor)));
        lastEnd = span.end;
      }
    }

    final endText = text.substring(lastEnd);
    if (endText.isNotEmpty) {
      spans.add(TextSpan(text: endText));
    }

    return TextSpan(style: style, children: spans);
  }

另外还有一个地方修改


image.png

每次插入或者文本变化之后,spanList必须要重新排序,这样才可以使用spanList来遍历,使用正则的好处就是不用排序,不过从最终优化的结果来看,还是排序比较容易看懂

不想看分析,直接拉取最新代码即可
代码传送门 https://gitee.com/Pino_W/flutter_ait

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

推荐阅读更多精彩内容

  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,711评论 2 59
  • 前言:很久没更新文章了,做个更新;前段时间,8 9月份在接触ml kit、机器学习、tensorflow相关的(比...
    Dynamic_2018阅读 14,106评论 1 17
  • 一、简介 1. 优点 2.缺点 二、环境搭建 1.sdk 配置 2.插件配置 3.镜像配置 三、混合开发...
    我卡苏总我阅读 1,343评论 0 0
  • 该文已授权公众号 「码个蛋」,转载请指明出处 前面的小节基本上讲完了常用的部件和容器部件,也可以完成很多的界面,但...
    Kuky_xs阅读 3,453评论 2 13
  • 前言 -- 这是一篇陆陆续续写了三天的文章 在实际的开发中我们经常会遇到用列表展示数据,当内容超过一屏的时候可以进...
    迷途小顽童阅读 4,934评论 0 3