模板支架图片开源seo软件
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 REGEXRegExp? allRegex;allRegex = patternMatchMap != null? RegExp(patternMatchMap?.keys.map((e) => e.pattern).join('|') ?? "",caseSensitive: regExpCaseSensitive,dotAll: regExpDotAll,multiLine: regExpMultiLine,unicode: regExpUnicode): null;// Validating with StringsRegExp? 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 matchfinal bool regExpCaseSensitive;/// controls the dotAll property of the full [RegExp] used to pattern matchfinal bool regExpDotAll;/// controls the multiLine property of the full [RegExp] used to pattern matchfinal bool regExpMultiLine;/// controls the unicode property of the full [RegExp] used to pattern matchfinal 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 REGEXRegExp? allRegex;allRegex = patternMatchMap != null? RegExp(patternMatchMap?.keys.map((e) => e.pattern).join('|') ?? "",caseSensitive: regExpCaseSensitive,dotAll: regExpDotAll,multiLine: regExpMultiLine,unicode: regExpUnicode): null;// Validating with StringsRegExp? 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 REGEXRegExp? allRegex;allRegex = patternMatchMap != null? RegExp(patternMatchMap?.keys.map((e) => e.pattern).join('|') ?? "",caseSensitive: regExpCaseSensitive,dotAll: regExpDotAll,multiLine: regExpMultiLine,unicode: regExpUnicode): null;// Validating with StringsRegExp? 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 controllerlate 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 stringsonMatch: (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
学习记录,每天不停进步。