Flutter RTL TextField添加Emoji后输入RTL字符乱码问题求助
问题描述
- 聊天模块实现底部弹出表情选择器,通过文本拼接方式将emoji添加到
TextEditingController,表情与文本混合在LTR模式下正常 - 适配阿拉伯语等RTL语言,文本方向可随首个RTL字符自动切换
- 仅安卓真机出现异常:TextField处于RTL模式时,添加至少一个emoji后输入RTL字符,最后一个Unicode字符会显示乱码(示例:
أهلا 🙃😂😇𒈩?) - iOS模拟器、安卓模拟器无此问题,使用
emoji_picker_flutter包后问题依旧 - 临时方案:在每个emoji后添加ZWSP(零宽空格,\u200C)可解决乱码,但用户需两次删除才能移除emoji,体验不佳
原因分析
安卓系统原生双向文本排版算法对emoji(中性Unicode字符)与RTL语言字符的组合处理存在兼容性缺陷:
- emoji属于中性字符,在RTL语境下会被双向排版算法归类为LTR方向
- 当emoji后紧跟RTL字符时,算法错误干扰了RTL字符的连写字形组合(阿拉伯语等RTL语言依赖字符连写渲染),导致最后一个字符被错误解析为乱码
解决方案
方案1:优化零宽字符删除体验(推荐)
在emoji后添加ZWSP的同时,监听TextEditingController的文本变化,当用户删除ZWSP时自动连带删除前面的emoji,实现一次删除即可移除emoji:
// 添加emoji的方法 void addEmoji(String emoji) { final currentText = widget.msg.text; // 拼接emoji和ZWSP final newText = '$currentText$emoji\u200C'; final newCursorPosition = newText.length; widget.msg.value = TextEditingValue( text: newText, selection: TextSelection.collapsed(offset: newCursorPosition), ); } // 初始化时添加文本监听 @override void initState() { super.initState(); widget.msg.addListener(_handleTextChange); } // 处理文本变化,优化删除逻辑 void _handleTextChange() { final text = widget.msg.text; final selection = widget.msg.selection; // 检查是否删除了ZWSP且前一个字符是emoji if (selection.start == selection.end && selection.start > 0) { final prevChar = text[selection.start - 1]; if (prevChar == '\u200C' && selection.start >= 2) { final charBeforeZwsp = text[selection.start - 2]; final emojiRune = charBeforeZwsp.runes.first; // 判断是否为emoji(覆盖常见emoji Unicode范围) if ((emojiRune >= 0x1F600 && emojiRune <= 0x1F64F) || (emojiRune >= 0x1F300 && emojiRune <= 0x1F5FF) || (emojiRune >= 0x2600 && emojiRune <= 0x26FF)) { // 同时删除emoji和ZWSP widget.msg.value = TextEditingValue( text: text.substring(0, selection.start - 2) + text.substring(selection.start), selection: TextSelection.collapsed(offset: selection.start - 2), ); } } } } @override void dispose() { widget.msg.removeListener(_handleTextChange); super.dispose(); }
方案2:使用双向控制字符替代ZWSP
在emoji后添加右到左标记(RLM,\u200F),强制后续文本保持RTL方向,避免排版算法出错,RLM视觉不可见,部分安卓输入法支持一次删除emoji+RLM:
void addEmoji(String emoji) { final currentText = widget.msg.text; final newText = '$currentText$emoji\u200F'; widget.msg.value = TextEditingValue( text: newText, selection: TextSelection.collapsed(offset: newText.length), ); }
方案3:自定义TextInputFormatter自动处理排版
通过TextInputFormatter拦截输入,自动在emoji后添加RLM,无需修改添加emoji的逻辑:
class RtlEmojiFormatter extends TextInputFormatter { @override TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { // 仅在RTL模式下处理 if (Directionality.of(WidgetsBinding.instance.context) == TextDirection.rtl) { final text = newValue.text; // 匹配常见emoji的正则 final emojiRegex = RegExp( r'[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]', unicode: true, ); // 在每个emoji后添加RLM final processedText = emojiRegex.replaceAllMapped(text, (match) => '${match.group(0)}\u200F'); return TextEditingValue( text: processedText, selection: TextSelection.collapsed(offset: processedText.length), composing: TextRange.empty, ); } return newValue; } } // 在TextField中添加formatter TextField( // ...其他原有属性 inputFormatters: [RtlEmojiFormatter()], )
验证建议
- 在安卓真机上测试上述方案,重点验证RTL字符连续输入、emoji删除的流畅性
- 针对阿拉伯语等连写语言,需测试多字符组合后的渲染效果
内容的提问来源于stack exchange,提问作者OKKO
相关产品推荐
相关产品推荐

