flutter聊天界面-TextField输入框实现@功能等匹配正则表达式展示高亮功能

2023-09-15 11:53:55

flutter聊天界面-TextField输入框实现@功能等匹配正则表达式展示高亮功能

一、简要描述

描述:
最近有位朋友讨论的时候,提到了输入框的高亮展示。在flutter TextField中需要插入特殊样式的标签,比如:“请 @张三 回答一下”,这一串字符在TextField中输入,当输入@时 弹出好友列表选择,然后将 “@张三”高亮显示在TextField中。

效果图如下

在这里插入图片描述
视频效果

flutter聊天界面-TextField输入框实现@功能

昨天整理的文中,简单实用TextEditingController的buildTextSpan时候,直接修改代码

List<InlineSpan> textSpans = RichTextHelper.getRichText(value.text);
    if (composingRegionOutOfRange) {
      return TextSpan(style: style, children: textSpans);
    }

会出现光标输入的问题,这里修改了一下,完善了一下。

大家可以使用rich_text_controller来实现,查看rich_text_controller源码可以看到,RichTextController继承TextEditingController,重写了buildTextSpan。经过我在iPhone上测试,当输入@汉字的时候,对中文兼容会有问题,这里做一下修改完善修改。

二、TextEditingController的buildTextSpan方法

在TextEditingController中buildTextSpan方法中,我们可以看到,该方法中的代码
在这里插入图片描述

composingRegionOutOfRange:仅有输入完成的字

在最后一部分的代码中含有未输入完成的字

final TextStyle composingStyle = style?.merge(const TextStyle(decoration: TextDecoration.underline))
        ?? const TextStyle(decoration: TextDecoration.underline);
    return TextSpan(
      style: style,
      children: <TextSpan>[
        TextSpan(text: value.composing.textBefore(value.text)),
        TextSpan(
          style: composingStyle,
          text: value.composing.textInside(value.text),
        ),
        TextSpan(text: value.composing.textAfter(value.text)),
      ],
    );
  • composingStyle未输入完成的字的样式,可以自己做下修改。

  • value.composing.textBefore:当前输入前面的字。

  • value.composing.textAfter:当前输入后面的字。

当输入过程中,我们将value.composing.textBefore,value.composing.textAfter匹配高亮即可

代码如下

value.composing.textBefore

TextSpan(style: style, children: buildRegExpSpan(context: context, text: value.composing.textBefore(value.text))),

value.composing.textAfter

TextSpan(style: style, children: buildRegExpSpan(context: context, text: value.composing.textAfter(value.text))),

匹配正则表达式

List<TextSpan> buildRegExpSpan(
      {required BuildContext context,
        TextStyle? style,
        required String? text}) {
    List<TextSpan> children = [];
    if (!(text != null && text.isNotEmpty)) {
      return children;
    }
    final matches = <String>{};
    List<Map<String, List<int>>> matchIndex = [];

    // Validating with REGEX
    RegExp? allRegex;
    allRegex = patternMatchMap != null
        ? RegExp(patternMatchMap?.keys.map((e) => e.pattern).join('|') ?? "",
        caseSensitive: regExpCaseSensitive,
        dotAll: regExpDotAll,
        multiLine: regExpMultiLine,
        unicode: regExpUnicode)
        : null;
    // Validating with Strings
    RegExp? stringRegex;
    stringRegex = stringMatchMap != null
        ? RegExp(r'\b' + stringMatchMap!.keys.join('|').toString() + r'+\$',
        caseSensitive: regExpCaseSensitive,
        dotAll: regExpDotAll,
        multiLine: regExpMultiLine,
        unicode: regExpUnicode)
        : null;
    
    text.splitMapJoin(
      stringMatchMap == null ? allRegex! : stringRegex!,
      onNonMatch: (String span) {
        if (stringMatchMap != null &&
            children.isNotEmpty &&
            stringMatchMap!.keys.contains("${children.last.text}$span")) {
          final String? ks =
          stringMatchMap!["${children.last.text}$span"] != null
              ? stringMatchMap?.entries.lastWhere((element) {
            return element.key
                .allMatches("${children.last.text}$span")
                .isNotEmpty;
          }).key
              : '';

          children.add(TextSpan(text: span, style: stringMatchMap![ks!]));
          return span.toString();
        } else {
          children.add(TextSpan(text: span, style: style));
          return span.toString();
        }
      },
      onMatch: (Match m) {
        matches.add(m[0]!);
        final RegExp? k = patternMatchMap?.entries.firstWhere((element) {
          return element.key.allMatches(m[0]!).isNotEmpty;
        }).key;

        final String? ks = stringMatchMap?[m[0]] != null
            ? stringMatchMap?.entries.firstWhere((element) {
          return element.key.allMatches(m[0]!).isNotEmpty;
        }).key
            : '';
        if (deleteOnBack!) {
          if ((isBack(text!, _lastValue) && m.end == selection.baseOffset)) {
            WidgetsBinding.instance.addPostFrameCallback((_) {
              children.removeWhere((element) => element.text! == text);
              text = text!.replaceRange(m.start, m.end, "");
              selection = selection.copyWith(
                baseOffset: m.end - (m.end - m.start),
                extentOffset: m.end - (m.end - m.start),
              );
            });
          } else {
            children.add(
              TextSpan(
                text: m[0],
                style: stringMatchMap == null
                    ? patternMatchMap![k]
                    : stringMatchMap![ks],
              ),
            );
          }
        } else {
          children.add(
            TextSpan(
              text: m[0],
              style: stringMatchMap == null
                  ? patternMatchMap![k]
                  : stringMatchMap![ks],
            ),
          );
        }
        final resultMatchIndex = matchValueIndex(m);
        if (resultMatchIndex != null && onMatchIndex != null) {
          matchIndex.add(resultMatchIndex);
          onMatchIndex!(matchIndex);
        }

        return (onMatch(List<String>.unmodifiable(matches)) ?? '');
      },
    );
    return children;
  }

这里使用的是rich_text_controller中的代码,做了相应的修改,输入@张三正则表达式正常高亮显示了。

整个text_field_controller代码如下

import 'package:flutter/material.dart';

class TextFieldController extends TextEditingController {
  final Map<RegExp, TextStyle>? patternMatchMap;
  final Map<String, TextStyle>? stringMatchMap;
  final Function(List<String> match) onMatch;
  final Function(List<Map<String, List<int>>>)? onMatchIndex;
  final bool? deleteOnBack;
  String _lastValue = "";

  /// controls the caseSensitive property of the full [RegExp] used to pattern match
  final bool regExpCaseSensitive;

  /// controls the dotAll property of the full [RegExp] used to pattern match
  final bool regExpDotAll;

  /// controls the multiLine property of the full [RegExp] used to pattern match
  final bool regExpMultiLine;

  /// controls the unicode property of the full [RegExp] used to pattern match
  final bool regExpUnicode;

  bool isBack(String current, String last) {
    return current.length < last.length;
  }

  TextFieldController(
      {String? text,
        this.patternMatchMap,
        this.stringMatchMap,
        required this.onMatch,
        this.onMatchIndex,
        this.deleteOnBack = false,
        this.regExpCaseSensitive = true,
        this.regExpDotAll = false,
        this.regExpMultiLine = false,
        this.regExpUnicode = false})
      : assert((patternMatchMap != null && stringMatchMap == null) ||
      (patternMatchMap == null && stringMatchMap != null)),
        super(text: text);

  /// Setting this will notify all the listeners of this [TextEditingController]
  /// that they need to update (it calls [notifyListeners]).
  
  set text(String newText) {
    value = value.copyWith(
      text: newText,
      selection: const TextSelection.collapsed(offset: -1),
      composing: TextRange.empty,
    );
  }

  /// Builds [TextSpan] from current editing value.
  
  TextSpan buildTextSpan(
      {required BuildContext context,
        TextStyle? style,
        required bool withComposing}) {
    assert(!value.composing.isValid || !withComposing || value.isComposingRangeValid);
    // If the composing range is out of range for the current text, ignore it to
    // preserve the tree integrity, otherwise in release mode a RangeError will
    // be thrown and this EditableText will be built with a broken subtree.
    final bool composingRegionOutOfRange = !value.isComposingRangeValid || !withComposing;

    if (composingRegionOutOfRange) {
      List<TextSpan> children = [];
      final matches = <String>{};
      List<Map<String, List<int>>> matchIndex = [];

      // Validating with REGEX
      RegExp? allRegex;
      allRegex = patternMatchMap != null
          ? RegExp(patternMatchMap?.keys.map((e) => e.pattern).join('|') ?? "",
          caseSensitive: regExpCaseSensitive,
          dotAll: regExpDotAll,
          multiLine: regExpMultiLine,
          unicode: regExpUnicode)
          : null;
      // Validating with Strings
      RegExp? stringRegex;
      stringRegex = stringMatchMap != null
          ? RegExp(r'\b' + stringMatchMap!.keys.join('|').toString() + r'+\$',
          caseSensitive: regExpCaseSensitive,
          dotAll: regExpDotAll,
          multiLine: regExpMultiLine,
          unicode: regExpUnicode)
          : null;
      
      text.splitMapJoin(
        stringMatchMap == null ? allRegex! : stringRegex!,
        onNonMatch: (String span) {
          if (stringMatchMap != null &&
              children.isNotEmpty &&
              stringMatchMap!.keys.contains("${children.last.text}$span")) {
            final String? ks =
            stringMatchMap!["${children.last.text}$span"] != null
                ? stringMatchMap?.entries.lastWhere((element) {
              return element.key
                  .allMatches("${children.last.text}$span")
                  .isNotEmpty;
            }).key
                : '';

            children.add(TextSpan(text: span, style: stringMatchMap![ks!]));
            return span.toString();
          } else {
            children.add(TextSpan(text: span, style: style));
            return span.toString();
          }
        },
        onMatch: (Match m) {
          matches.add(m[0]!);
          final RegExp? k = patternMatchMap?.entries.firstWhere((element) {
            return element.key.allMatches(m[0]!).isNotEmpty;
          }).key;

          final String? ks = stringMatchMap?[m[0]] != null
              ? stringMatchMap?.entries.firstWhere((element) {
            return element.key.allMatches(m[0]!).isNotEmpty;
          }).key
              : '';
          if (deleteOnBack!) {
            if ((isBack(text, _lastValue) && m.end == selection.baseOffset)) {
              WidgetsBinding.instance.addPostFrameCallback((_) {
                children.removeWhere((element) => element.text! == text);
                text = text.replaceRange(m.start, m.end, "");
                selection = selection.copyWith(
                  baseOffset: m.end - (m.end - m.start),
                  extentOffset: m.end - (m.end - m.start),
                );
              });
            } else {
              children.add(
                TextSpan(
                  text: m[0],
                  style: stringMatchMap == null
                      ? patternMatchMap![k]
                      : stringMatchMap![ks],
                ),
              );
            }
          } else {
            children.add(
              TextSpan(
                text: m[0],
                style: stringMatchMap == null
                    ? patternMatchMap![k]
                    : stringMatchMap![ks],
              ),
            );
          }
          final resultMatchIndex = matchValueIndex(m);
          if (resultMatchIndex != null && onMatchIndex != null) {
            matchIndex.add(resultMatchIndex);
            onMatchIndex!(matchIndex);
          }

          return (onMatch(List<String>.unmodifiable(matches)) ?? '');
        },
      );

      _lastValue = text;
      return TextSpan(style: style, children: children);
    }

    final TextStyle composingStyle = style?.merge(const TextStyle(decoration: TextDecoration.underline))
        ?? const TextStyle(decoration: TextDecoration.underline);
    return TextSpan(
      children: <TextSpan>[
        TextSpan(style: style, children: buildRegExpSpan(context: context, text: value.composing.textBefore(value.text))),
        TextSpan(
          style: composingStyle,
          text: value.composing.textInside(value.text),
        ),
        TextSpan(style: style, children: buildRegExpSpan(context: context, text: value.composing.textAfter(value.text))),
      ],
    );
  }

  Map<String, List<int>>? matchValueIndex(Match match) {
    final matchValue = match[0]?.replaceFirstMapped('#', (match) => '');
    if (matchValue != null) {
      final firstMatchChar = match.start + 1;
      final lastMatchChar = match.end - 1;
      final compactMatch = {
        matchValue: [firstMatchChar, lastMatchChar]
      };
      return compactMatch;
    }
    return null;
  }

  List<TextSpan> buildRegExpSpan(
      {required BuildContext context,
        TextStyle? style,
        required String? text}) {
    List<TextSpan> children = [];
    if (!(text != null && text.isNotEmpty)) {
      return children;
    }
    final matches = <String>{};
    List<Map<String, List<int>>> matchIndex = [];

    // Validating with REGEX
    RegExp? allRegex;
    allRegex = patternMatchMap != null
        ? RegExp(patternMatchMap?.keys.map((e) => e.pattern).join('|') ?? "",
        caseSensitive: regExpCaseSensitive,
        dotAll: regExpDotAll,
        multiLine: regExpMultiLine,
        unicode: regExpUnicode)
        : null;
    // Validating with Strings
    RegExp? stringRegex;
    stringRegex = stringMatchMap != null
        ? RegExp(r'\b' + stringMatchMap!.keys.join('|').toString() + r'+\$',
        caseSensitive: regExpCaseSensitive,
        dotAll: regExpDotAll,
        multiLine: regExpMultiLine,
        unicode: regExpUnicode)
        : null;
    
    text.splitMapJoin(
      stringMatchMap == null ? allRegex! : stringRegex!,
      onNonMatch: (String span) {
        if (stringMatchMap != null &&
            children.isNotEmpty &&
            stringMatchMap!.keys.contains("${children.last.text}$span")) {
          final String? ks =
          stringMatchMap!["${children.last.text}$span"] != null
              ? stringMatchMap?.entries.lastWhere((element) {
            return element.key
                .allMatches("${children.last.text}$span")
                .isNotEmpty;
          }).key
              : '';

          children.add(TextSpan(text: span, style: stringMatchMap![ks!]));
          return span.toString();
        } else {
          children.add(TextSpan(text: span, style: style));
          return span.toString();
        }
      },
      onMatch: (Match m) {
        matches.add(m[0]!);
        final RegExp? k = patternMatchMap?.entries.firstWhere((element) {
          return element.key.allMatches(m[0]!).isNotEmpty;
        }).key;

        final String? ks = stringMatchMap?[m[0]] != null
            ? stringMatchMap?.entries.firstWhere((element) {
          return element.key.allMatches(m[0]!).isNotEmpty;
        }).key
            : '';
        if (deleteOnBack!) {
          if ((isBack(text!, _lastValue) && m.end == selection.baseOffset)) {
            WidgetsBinding.instance.addPostFrameCallback((_) {
              children.removeWhere((element) => element.text! == text);
              text = text!.replaceRange(m.start, m.end, "");
              selection = selection.copyWith(
                baseOffset: m.end - (m.end - m.start),
                extentOffset: m.end - (m.end - m.start),
              );
            });
          } else {
            children.add(
              TextSpan(
                text: m[0],
                style: stringMatchMap == null
                    ? patternMatchMap![k]
                    : stringMatchMap![ks],
              ),
            );
          }
        } else {
          children.add(
            TextSpan(
              text: m[0],
              style: stringMatchMap == null
                  ? patternMatchMap![k]
                  : stringMatchMap![ks],
            ),
          );
        }
        final resultMatchIndex = matchValueIndex(m);
        if (resultMatchIndex != null && onMatchIndex != null) {
          matchIndex.add(resultMatchIndex);
          onMatchIndex!(matchIndex);
        }

        return (onMatch(List<String>.unmodifiable(matches)) ?? '');
      },
    );
    return children;
  }
}

至此可以看到效果图中@张三 高亮显示了。

三、使用TextFieldController测试@张三 高亮

调整好TextFieldController后,我这里测试@张三 高亮

我们进行初始化TextFieldController

// Add a controller
  late TextFieldController _controller;

  
  void initState() {
    // TODO: implement initState
    _controller = TextFieldController(
      patternMatchMap: {
        //
        //* Returns every Hashtag with red color
        //
        RegExp(r"@[^\s]+\s?"):TextStyle(color:Colors.green),
        //
        //* Returns every Hashtag with red color
        //
        RegExp(r"\B#[a-zA-Z0-9]+\b"):TextStyle(color:Colors.red),
        //
        //* Returns every Mention with blue color and bold style.
        //
        RegExp(r"\B@[a-zA-Z0-9]+\b"):TextStyle(fontWeight: FontWeight.w800 ,color:Colors.blue,),
        //
        //* Returns every word after '!' with yellow color and italic style.
        //
        RegExp(r"\B![a-zA-Z0-9]+\b"):TextStyle(color:Colors.yellow, fontStyle:FontStyle.italic),
        // add as many expressions as you need!
      },
      //* starting v1.2.0
      // Now you have the option to add string Matching!
      // stringMatchMap: {
      //   "String1":TextStyle(color: Colors.red),
      //   "String2":TextStyle(color: Colors.yellow),
      // },
      //! Assertion: Only one of the two matching options can be given at a time!

      //* starting v1.1.0
      //* Now you have an onMatch callback that gives you access to a List<String>
      //* which contains all matched strings
      onMatch: (List<String> matches){
        // Do something with matches.
        //! P.S
        // as long as you're typing, the controller will keep updating the list.
      },
      deleteOnBack: true,
      // You can control the [RegExp] options used:
      regExpUnicode: true,
    );

    super.initState();
  }

在TextField中使用TextFieldController。具体代码如下

TextField(
        minLines: 1,
        maxLines: null,
        keyboardType: TextInputType.multiline,
        textAlignVertical: TextAlignVertical.center,
        autofocus: true,
        focusNode: editFocusNode,
        controller: _controller,
        textInputAction: TextInputAction.send,
        decoration: InputDecoration(
          contentPadding: EdgeInsets.symmetric(vertical: 10, horizontal: 8.0),
          filled: true,
          isCollapsed: true,
          floatingLabelBehavior: FloatingLabelBehavior.never,
          hintText: "说点什么吧~",
          hintStyle: TextStyle(
            fontSize: 14,
            fontWeight: FontWeight.w400,
            fontStyle: FontStyle.normal,
            color: ColorUtil.hexColor(0xACACAC),
            decoration: TextDecoration.none,
          ),
          enabledBorder: OutlineInputBorder(
            /*边角*/
            borderRadius: const BorderRadius.all(
              Radius.circular(5.0), //边角为30
            ),
            borderSide: BorderSide(
              color: ColorUtil.hexColor(0xf7f7f7), //边框颜色为绿色
              width: 1, //边线宽度为1
            ),
          ),
          focusedBorder: OutlineInputBorder(
            borderRadius: const BorderRadius.all(
              Radius.circular(5.0), //边角为30
            ),
            borderSide: BorderSide(
              color: ColorUtil.hexColor(0xECECEC), //边框颜色为绿色
              width: 1, //宽度为1
            ),
          ),
        ),
      )

经过输入删除测试,输入的“@张三”高亮显示在TextField中正常了。

使用TextEditingController的buildTextSpan,可以查看:https://blog.csdn.net/gloryFlow/article/details/132889374
完善TextField输入框匹配正则表达式高亮显示,可以查看:https://blog.csdn.net/gloryFlow/article/details/132899084

四、小结

flutter聊天界面-TextField输入框buildTextSpan实现@功能展示高亮功能。自定义修改TextEditingController。
内容较多,描述可能不准确,请见谅。

本文地址:https://blog.csdn.net/gloryFlow/article/details/132899084

学习记录,每天不停进步。

更多推荐

VR虚拟仿真在旅游课堂教学演示

首先,VR虚拟仿真能够为学生提供逼真的旅游体验。传统的旅游课堂教学主要以图片、文字和视频为主要教学工具,这无法给学生带来身临其境的感觉。而VR技术能够通过360度全景视角、立体声音和触觉反馈等功能,将学生置身于虚拟的旅游场景中。无论是登上古老的埃及金字塔,在巴黎卢浮宫欣赏名画,还是漫步在纽约时代广场的繁华街头,学生可以

Mysql高级——索引优化和查询优化(2)

5.排序优化5.1排序优化问题:在WHERE条件字段上加索引,但是为什么在ORDERBY字段上还要加索引呢?优化建议:SQL中,可以在WHERE子句和ORDERBY子句中使用索引,目的是在WHERE子句中避免全表扫描,在ORDERBY子句避免使用FileSort排序。当然,某些情况下全表扫描,或者FileSort排序不

关于安卓SVGA浅尝(二)加载数据

关于安卓SVGA浅尝(二)加载数据相关链接SVGA官网SVGA-github说明文档背景项目开发,都会和动画打交道,动画的方案选取,就有很多选择。如Json动画,svga动画,gif等等。各有各的优势。目前项目中用到了svga的动画,因此,就有了这一系列的文章。实现对于svga的加载方法,有以下几种:(1)decode

VR全景智慧文旅解决方案,助力文旅产业转型升级

引言:随着科技的不断发展,虚拟现实(VR)技术正逐渐展露其影响力,改变着旅游业。VR全景智慧文旅解决方案也应运而生,将传统旅游的体验形式从线下扩展到了线上,带来了不一般的文旅体验。一.VR全景技术的基础VR全景技术是通过结合虚拟现实和全景摄影技术来模拟现实世界的方法。通过智能设备,游客可以在任何时间,任何地点进入逼真的

5个超实用的Python代码片段

迷途小书童读完需要7分钟速读仅需3分钟本篇收集了5个常用的Python代码片段,非常实用,可以帮助我们更高效的编写代码,实现我们想要实现的功能。1密码生成器importrandomimportstringtotal=string.ascii_letters+string.digits+string.punctuatio

内网横向移动

内网横向移动当攻击者在拿下一台内网主机后,通常会利用当前拿下的机器当作跳板,进一步攻击内网其他主机,扩大攻击影响范围。攻击机:KaliLinux靶机:Windowsserver2008WEB10.10.10.20\192.168.XX.XX(跳板机)Windowsserver2012DC10.10.10.10\192.

RS485总线浪涌解决方案!|深圳比创达EMC

在各种通讯方式当中,RS485总线是较为常见的一种,因其接口简单、系统运行稳定和通讯速率高等特点被广泛应用。但由于RS485总线一般传输距离较长,且经常暴露在外,所以非常容易受到电磁干扰,而浪涌干扰已经成为485通信使用中最常遇到的问题。下面通过一个实际案例,来说明RS485总线在防浪涌实验过程中,常遇到的问题以及需要

shell中[[]]与[],=、==和-eq的辨析

1、==、=和-eq在shell中,=和==运算符都可以用于判断两个字符串、两个字符串变量是否相同,==支持模式匹配,而=不支持模式匹配。使用-eq来判断两个整数是否相等。#字符串比较#给变量赋值时,等号前后没有空格,有空格时是条件判断string1=abcstring2=bcd#["$string1"="$strin

基座向量施密特正交化

最近再次细细的阅读了向量施密特正交化,重新系统梳理一下一、正交基地与向量的正交分解二、基化成标准正交基,是什么意思将一个向量空间中的基向量通过某种方式转化为一组标准正交基,是指将原有的基向量进行调整,使得它们满足两个条件:彼此之间两两正交(即内积为零);归一化(长度为1)。这样的转换可以用来简化向量空间中的运算,使得向

Linux 本地Yearning SQL 审核平台远程访问

文章目录前言1.Linux部署Yearning2.本地访问Yearning3.Linux安装cpolar4.配置Yearning公网访问地址5.公网远程访问Yearning管理界面6.固定Yearning公网地址前言Yearning简单,高效的MYSQL审计平台一款MYSQLSQL语句/查询审计工具,为DBA与开发人员

Redis的主从复制,哨兵和Cluster集群

一、Redis的高可用1.1redis高可用的概念在web服务器中,高可用是指服务器可以正常访问的时间,衡量的标准是在多长时间内可以提供正常服务(99.9%、99.99%、99.999%等等)。高可用的计算公式是1-(宕机时间)/(宕机时间+运行时间)有点类似与网络传输的参数误码率,我们用9的个数表示可用性:2个9:9

热文推荐