一.富文本正则匹配测试网站:regexr.com/ 1,自定义格式 表情: '[:smile]','[:winking]' 通过正则匹配到对应的图片
艾特功能: (@[\w\d-\u4e00-\u9fa5]+#\d+#)
匹配例如@开头的符合格式的一串字符:@zhoujielun#1#
2,匹配输出展示 使用TextSpan 的 InlineSpan进行展示: 输入框 :ExtendedTextField配合specialTextSpanBuilder展示
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:meetu_pro/common/utils/logger_util.dart';
import '../../../modules/chat/utils/image_util.dart';
import 'emoji_text.dart';
import 'emoji_view.dart';
typedef AtTextCallback = Function(String showText, String actualText);
class EmojiSpecialTextSpanBuilder extends SpecialTextSpanBuilder {
final AtTextCallback? atCallback;
final Map<String, String> allAtMap;
final TextStyle? atStyle;
EmojiSpecialTextSpanBuilder({
this.atCallback,
this.atStyle,
this.allAtMap = const <String, String>{},
});
@override
TextSpan build(
String data, {
TextStyle? textStyle,
SpecialTextGestureTapCallback? onTap,
}) {
StringBuffer buffer = StringBuffer();
if (kIsWeb) {
return TextSpan(text: data, style: textStyle);
}
final List<InlineSpan> children = <InlineSpan>[];
var regexEmoji = emojiFaces.keys
.toList()
.join('|')
.replaceAll('[', '\\[')
.replaceAll(']', '\\]');
final list = [regexAt, regexEmoji];
final pattern = '(${list.toList().join('|')})';
final atReg = RegExp(regexAt);
final emojiReg = RegExp(regexEmoji);
data.splitMapJoin(
RegExp(pattern),
onMatch: (Match m) {
late InlineSpan inlineSpan;
String value = m.group(0)!;
try {
if (atReg.hasMatch(value)) {
String? name = value.split('#').first;
inlineSpan = SpecialTextSpan(
text: name,
actualText: value,
start: m.start,
style: atStyle,
);
buffer.write('$name ');
} else if (emojiReg.hasMatch(value)) {
inlineSpan = ImageSpan(
ImageUtil.emojiImage(value),
imageWidth: 20.w,
imageHeight: 20.w,
start: m.start,
actualText: value,
);
} else {
inlineSpan = TextSpan(text: value, style: textStyle);
buffer.write(value);
}
} catch (e) {
printDebugLog('error: $e');
}
children.add(inlineSpan);
return '';
},
onNonMatch: (text) {
children.add(TextSpan(text: text, style: textStyle));
buffer.write(text);
return '';
},
);
if (null != atCallback) atCallback!(buffer.toString(), data);
return TextSpan(children: children, style: textStyle);
}
@override
SpecialText? createSpecialText(
String flag, {
TextStyle? textStyle,
SpecialTextGestureTapCallback? onTap,
required int index,
}) {
return null;
}
}
text文本展示 ,自定义text视图:
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:meetu_pro/modules/chat/utils/image_util.dart';
import 'emoji_view.dart';
/// message content: @uid1 @uid2 xxxxxxx
///
enum ChatTextModel { match, normal }
class EmojiText extends StatelessWidget {
final String text;
final TextStyle? textStyle;
final InlineSpan? prefixSpan;
/// isReceived ? TextAlign.left : TextAlign.right
final TextAlign textAlign;
final TextOverflow overflow;
final int? maxLines;
final double textScaleFactor;
/// all user info
/// key:userid
/// value:username
final Map<String, String> allAtMap;
final List<MatchPattern> patterns;
final ChatTextModel model;
// final TextAlign textAlign;
const EmojiText({
Key? key,
required this.text,
this.allAtMap = const <String, String>{},
this.prefixSpan,
this.patterns = const <MatchPattern>[],
this.textAlign = TextAlign.left,
this.overflow = TextOverflow.clip,
this.textStyle,
this.maxLines,
this.textScaleFactor = 1.0,
this.model = ChatTextModel.match,
}) : super(key: key);
static final _textStyle = TextStyle(
fontSize: 14.sp,
color: const Color(0xFF333333),
);
@override
Widget build(BuildContext context) {
final List<InlineSpan> children = <InlineSpan>[];
if (prefixSpan != null) children.add(prefixSpan!);
if (model == ChatTextModel.normal) {
_normalModel(children);
} else {
_matchModel(children);
}
return Container(
constraints: BoxConstraints(maxWidth: 0.5.sw),
child: RichText(
textAlign: textAlign,
overflow: overflow,
maxLines: maxLines,
textScaleFactor: textScaleFactor,
text: TextSpan(children: children),
),
);
}
_normalModel(List<InlineSpan> children) {
var style = textStyle ?? _textStyle;
children.add(TextSpan(text: text, style: style));
}
_matchModel(List<InlineSpan> children) {
var style = textStyle ?? _textStyle;
final _mapping = Map<String, MatchPattern>();
patterns.forEach((e) {
if (e.type == PatternType.at) {
_mapping[regexAt] = e;
} else if (e.type == PatternType.email) {
_mapping[regexEmail] = e;
} else if (e.type == PatternType.mobile) {
_mapping[regexMobile] = e;
} else if (e.type == PatternType.tel) {
_mapping[regexTel] = e;
} else if (e.type == PatternType.url) {
_mapping[regexUrl] = e;
} else {
_mapping[e.pattern!] = e;
}
});
var regexEmoji = emojiFaces.keys
.toList()
.join('|')
.replaceAll('[', '\\[')
.replaceAll(']', '\\]');
_mapping[regexEmoji] = MatchPattern(type: PatternType.emoji);
_mapping[regexAt] = MatchPattern(type: PatternType.at);
final pattern;
if (_mapping.length > 1) {
pattern = '(${_mapping.keys.toList().join('|')})';
} else {
pattern = regexEmoji;
}
// match text
stripHtmlIfNeeded(text).splitMapJoin(
RegExp(pattern),
onMatch: (Match match) {
var matchText = match[0]!;
var value = matchText;
var inlineSpan;
final mapping = _mapping[matchText] ??
_mapping[_mapping.keys.firstWhere((element) {
final reg = RegExp(element);
return reg.hasMatch(matchText);
}, orElse: () {
return '';
})];
if (mapping != null) {
if (mapping.type == PatternType.at) {
// String uid = matchText.replaceFirst("@", "").trim();
// value = uid;
// if (allAtMap.containsKey(uid)) {
// matchText = '@${allAtMap[uid]!} ';
// }
String name = matchText.split('#').first;
value = name;
if (name.isNotEmpty) {
matchText = name;
}
inlineSpan = TextSpan(
text: matchText,
style: const TextStyle(color: Color(0xff21D684)));
}
if (mapping.type == PatternType.emoji) {
inlineSpan = ImageSpan(
ImageUtil.emojiImage(matchText),
imageWidth: style.fontSize!,
imageHeight: style.fontSize!,
);
} else {
inlineSpan = TextSpan(
text: matchText,
style: mapping.style ?? style,
recognizer: mapping.onTap == null
? null
: (TapGestureRecognizer()
..onTap = () => mapping.onTap!(
_getUrl(value, mapping.type), mapping.type)),
);
}
} else {
inlineSpan = TextSpan(text: matchText, style: style);
}
children.add(inlineSpan);
return '';
},
onNonMatch: (text) {
children.add(TextSpan(text: text, style: style));
return '';
},
);
}
_getUrl(String text, PatternType type) {
switch (type) {
case PatternType.url:
return text.substring(0, 4) == 'http' ? text : 'http://$text';
case PatternType.email:
return text.substring(0, 7) == 'mailto:' ? text : 'mailto:$text';
case PatternType.tel:
case PatternType.mobile:
return text.substring(0, 4) == 'tel:' ? text : 'tel:$text';
// case PatternType.PHONE:
// return text.substring(0, 4) == 'tel:' ? text : 'tel:$text';
default:
return text;
}
}
static String stripHtmlIfNeeded(String text) {
return text.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ' ');
}
}
class MatchPattern {
PatternType type;
String? pattern;
TextStyle? style;
Function(String link, PatternType? type)? onTap;
MatchPattern({required this.type, this.pattern, this.style, this.onTap});
}
enum PatternType { at, email, mobile, tel, url, emoji, custom }
/// 空格@uid空格 @xxx-
/// r"(\s@\S+\-)"
const regexAt = r"(@[\w\d\-\u4e00-\u9fa5]+#\d+#)";
/// Email Regex - A predefined type for handling email matching
const regexEmail = r"\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b";
/// URL Regex - A predefined type for handling URL matching
const regexUrl =
r"[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:._\+-~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:_\+.~#?&\/\/=]*)";
// Regex of exact mobile.
const String regexMobile =
'^(\\+?86)?((13[0-9])|(14[57])|(15[0-35-9])|(16[2567])|(17[01235-8])|(18[0-9])|(19[1589]))\\d{8}\$';
/// Regex of telephone number.
const String regexTel = '^0\\d{2,3}[-]?\\d{7,8}';