美文网首页
Flutter-输入框潜在bug

Flutter-输入框潜在bug

作者: Magic旭 | 来源:发表于2020-04-27 17:10 被阅读0次

问题

问题描述:当Flutter的输入框中支持上了表情符号(emoji),无论你用maxLength还是inputFormatters属性,都会出现长度超过你给定的值或表达式,而且光标还会在达到最后字符的时候往前移动一个字符。以下都是围绕当输入框有表情符号开展的。

问题原因:原本我是以为光标的rect数据,所以在text_painter.dart类看了Rect _getRectFromDownstream(int offset, Rect caretPrototype)方法,用来计算光标的矩阵。(文字属性是downStream的方法走这个方法,有对应的一个是upstream的方法的)然后在追查到最后的editable.dart的_paintCaretPaint方法绘制里,发现出错在rect矩阵数据里,绘制没有任何问题。然后回到计算光标矩阵的地方,在观察到往前移动的光标异常问题是因为offset引起的。

根本原因:无论是maxLength还是inputFormatters最后都是以inputFormatters作为EditableText的构造函数,在EditableText没有处理好限字符的问题。这里主要就是看LengthLimitingTextInputFormatter类了,因为EditableText都是用各种inputFormatters处理文案的问题。


_formatAndSetValue方法

字符比限制的数字多一个原因:由于没有处理好最大字符数的问题,当flutter/engine的原生输入框数据返回时候就是用户真实输入的数据,所以到了EditableText更新text也是不做长度判断,所以才会导致比限制的数字多一个的问题。

光标前移原因:这里是因为flutter层在计算最大字符数时候,触发刷新,native又返回一次输入框的数据,这里可以在text.input.dart类中_handleTextInputInvocation(MethodCall methodCall)中返回数据解析生成的TextEditingValue中光标位置就是用户输入的maxLength。

如何解决

通过上面的问题大概知道就是当文字中混有表情符号(emoji)时候,flutter计算text时候就会出现漏洞。这里先补给下最重要的知识点,全面认识TextEditingValue类。

TextEditingValue

TextEditingValue类是所有输入框封装文案与光标的基类。TextEditingValue里面的属性分别都有自己用处。
text:输入框要展示的文案。
作用:展示文案的内容。
selection:TextSelection类(affinity:文字的TextAffinity属性;baseOffset:字符开始的偏移量(有光标情况:光标的起始位置);extentOffset:字符插入的位置(有光标情况:光标的结束位置)
作用:用户选择插入字符的位置。
composing:TextRange类(int start,int end)。start是文本编辑的起始位置,end是文本编辑的结束位置。
作用:用来判断该文本是否还在编辑状态,如果还在编辑状态,native层在_handleTextInputInvocation返回时候,将不带上TextRange的start和end标识的字符串。(这个属性专门用来处理iOS自带的输入框问题,会在章节《遇到的问题》中描述,坑爹)


解决办法
  1. 从源码中能看到EditableText每次接收到新的字符串内容,都会在_formatAndSetValue方法调用onChanged(String text)方法。其中可以看到这里inputFormatters没有处理好带表情的TextEditingValue,所以我们要做的就是在onChange方法中处理好TextEditingValue。


    _formatAndSetValue方法
  • 方法一(能达到超长的回调,也是比较挫的方法)
  1. 最好自己封装一个XXXTextField类,统一处理这类问题。或者自己写一个inputFormatters,把官网的弥补下。现在我先介绍我自己比较挫的解决办法先。
在封装的类中增加两个属性
//最大长度
final int selfMaxLength;
//达到最大长度后的回调
final Function maxLengthCallBack

class _XXXTextFieldState extends State<XXXTextField> {
      ……
      //是否已经触发过一次超长
      bool hasAlreadyMaxLength = false;
      //第一次触发超长的文案
      String lastMaxContent = "";
      
      @override
      void initState() {
          if (widget.controller.text.length == widget.selfMaxLength) {
                  //UI带进来的文案超长,记录
                  hasAlreadyMaxLength = true;
                  lastMaxContent = widget.controller.text;
            } else {
                   //UI带进来的文案没有超长
                  hasAlreadyMaxLength = false;
                  lastMaxContent = "";
            }
      }
      //maxLength or inputFormatters正常设置不影响
      @override
      Widget build(BuildContext context) {
            return Container(
                  child: TextField(
                  key: widget.key,
                  ……
                  onChanged: (String value) {
                    setState(() {});
                    if (widget.onChanged != null) {
                      widget.onChanged(value);
                    }
                    _actionMaxLengthState(value);
                  },
                  ……
                )
            )
      }
    
    /***真正处理逻辑***/
   //第一次文案超长情况下,把光标定位到最后一个符号后面,裁切native返回的文案
   void _resetSelection(String newText) {
    var sRunes = newText.runes;
    String result;
    for (int i = 0; i < sRunes.length; i++) {
      if (String.fromCharCodes(sRunes, 0, sRunes.length - i).length <= widget.selfMaxLength) {
        result = String.fromCharCodes(sRunes, 0, sRunes.length - i);
        if (result.runes.last == 105) {
          //如果删除后剩下的还有一个空格,继续删除
          result = String.fromCharCodes(result.runes, 0, result.runes.length - 1);
        }
        break;
      }
    }
    TextSelection temp = widget.controller.value.selection.copyWith(
      baseOffset: result.length,
      extentOffset: result.length,
    );
    TextRange fixRange = widget.controller.value.composing;
    if (widget.controller.value.composing.end > result.length) {
      fixRange = TextRange(start: fixRange.start, end: result.length);
    }
    widget.controller.value = TextEditingValue(text: result, selection: temp, composing: fixRange);
    lastMaxContent = result;
  }

  //当用户在超长情况下,继续怎样输入,显示第一次超长的文案,把光标定位到最后个字符位置。
  void _initOldDataSelection(String newText) {
    TextSelection actualSelection = widget.controller.value.selection;
    actualSelection = widget.controller.value.selection.copyWith(
      baseOffset: lastMaxContent.length,
      extentOffset: lastMaxContent.length,
    );
    //ios:当TextRange不为-1时候,下次update会把start和end直接的变量值全部丢弃。当你确定内容不变时候,请把他们变成-1
    TextRange fixRange = TextRange(start: -1, end: -1);
    widget.controller.value = TextEditingValue(text: lastMaxContent, selection: actualSelection, composing: fixRange);
  }

  
  void _actionMaxLengthState(String newText) {
    if (newText.length >= widget.selfMaxLength) {
      if (widget.maxLengthCallBack != null) widget.maxLengthCallBack();
      if (hasAlreadyMaxLength) {
        _initOldDataSelection(newText);
      } else {
        hasAlreadyMaxLength = true;
        _resetSelection(newText);
      }
    } else {
      //对controller的text设置,一定要是对value改变,要不然直接设置text,selection就是为-1默认值。
      widget.controller.value =
          TextEditingValue(text: newText, selection: widget.controller.value.selection, composing: widget.controller.value.composing);
      //如果用户删除文案,达不到超长效果,就清除超长数据
      hasAlreadyMaxLength = false;
      lastMaxContent = "";
    }
  }
}
  1. 调用时候就用封装的XXXTextField类,然后添加selfMaxLength最大长度和maxLengthCallBack最大长度返回参数(callBack自己看业务吧)。


    调用示例
  • 方法二:
    自定义LengthLimitingTextInputFormatter,道理和上面挫的方法差不多,这里就不一一解析了。如果用这个也要获取超长回调,直接设置个callBack即可,直接看代码
import 'package:flutter/material.dart';
import 'dart:math' as math;

import 'package:flutter/services.dart';

class MTLengthLimitingTextInputFormatter extends TextInputFormatter {
  MTLengthLimitingTextInputFormatter(this.maxLength) : assert(maxLength == null || maxLength == -1 || maxLength > 0);

  final int maxLength;

  bool hasAlreadyMaxLength = false;
///超过文本长度回调
final Function maxLengthCallBack;

  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue, // unused.
    TextEditingValue newValue,
  ) {
    hasAlreadyMaxLength = oldValue.text.length >= maxLength;
    if (!hasAlreadyMaxLength && maxLength != null && maxLength > 0 && newValue.text.length >= maxLength) {
      ///第一次超长
      if(maxLengthCallBack != null) maxLengthCallBack();
      return _resetSelection(newValue);
    } else if (hasAlreadyMaxLength && maxLength != null && maxLength > 0 && newValue.text.length >= maxLength) {
      ///第二次往后超长
      if(maxLengthCallBack != null) maxLengthCallBack();
      return _initOldDataSelection(oldValue, newValue);
    } else {
     hasAlreadyMaxLength = false;
      return newValue;
    }
  }

  TextEditingValue _resetSelection(TextEditingValue newValue) {
    hasAlreadyMaxLength = true;
    var sRunes = newValue.text.runes;
    String result;
    int i = 0;
    for (i = 0; i < sRunes.length; i++) {
      if (String.fromCharCodes(sRunes, 0, sRunes.length - i).length <= maxLength) {
        result = String.fromCharCodes(sRunes, 0, sRunes.length - i);
        if (result.runes.last == 105) {
          //如果删除后剩下的还有一个空格,继续删除
          result = String.fromCharCodes(result.runes, 0, result.runes.length - 1);
        }
        break;
      }
    }
    TextSelection temp = newValue.selection.copyWith(
      baseOffset: result.length,
      extentOffset: result.length,
    );
    TextRange fixRange = newValue.composing;
    if (newValue.composing.end > result.length) {
      fixRange = TextRange(start: fixRange.start - i, end: result.length);
    }
    return TextEditingValue(text: result, selection: temp, composing: fixRange);
  }

  TextEditingValue _initOldDataSelection(TextEditingValue oldValue, TextEditingValue newValue) {
    TextSelection actualSelection = newValue.selection;
    actualSelection = newValue.selection.copyWith(
      baseOffset: oldValue.text.length,
      extentOffset: oldValue.text.length,
    );
    //ios:当TextRange不为-1时候,下次update会把start和end直接的变量值全部丢弃。当你确定内容不变时候,请把他们变成-1
    TextRange fixRange = TextRange(start: -1, end: -1);
    return TextEditingValue(text: oldValue.text, selection: actualSelection, composing: fixRange);
  }
}


遇到的问题

一、iOS自带原生输入框,当用户输入拼音,点击中文的时候,输入框中会显示类似:w d我的。这种明显处于bug的问题让我一个安卓菜鸡一脸懵逼。后面仔细看了源码的参数,才发现我的TextEditingValue少了一个composing属性,这个属性就是用来告诉 iOS 的native层哪些文案还在编辑,等用户选中要的文案后便可删除。所以后面我给加了composing属性。
出问题代码(就用_actionMaxLengthState举例子,其他函数看上面解决办法即可):

void _actionMaxLengthState(String newText) {
    if (newText.length >= widget.selfMaxLength) {
     ……
    } else {
      //对controller的text设置,一定要是对value改变,要不然直接设置text,selection就是为-1默认值。
      widget.controller.value = TextEditingValue(text: newText, selection: widget.controller.value.selection);
      hasAlreadyMaxLength = false;
      lastMaxContent = "";
    }
  }

修复后的代码:

void _actionMaxLengthState(String newText) {
    if (newText.length >= widget.selfMaxLength) {
        ……
    } else {
      //对controller的text设置,一定要是对value改变,要不然直接设置text,selection就是为-1默认值。
      widget.controller.value =
          TextEditingValue(text: newText, selection: widget.controller.value.selection, composing: widget.controller.value.composing);
      hasAlreadyMaxLength = false;
      lastMaxContent = "";
    }
  }

二、在第一个问题解决后,紧接着在iOS上遇到第二个问题。当用户多次输入超长内容后(内容中包含表情),会出现表情后的文案全部消失。这里出现的问题还是composing,没有完全理解含义,直接使用native返回的composing所致。
出问题代码(_initOldDataSelection方法):

void _initOldDataSelection(String newText) {
    ……
    widget.controller.value = TextEditingValue(text: lastMaxContent, selection: actualSelection, composing: widget.controller.value.composing);
  }

修复后的代码(修复后的意思即是用户多次超长的内容,我帮用户恢复,属于文本不在编辑状态,iOS的native层不能把我的内容删除):

void _initOldDataSelection(String newText) {
    ……
    //ios:当TextRange不为-1时候,下次update会把start和end直接的变量值全部丢弃。当你确定内容不变时候,请把他们变成-1
    TextRange fixRange = TextRange(start: -1, end: -1);
    widget.controller.value = TextEditingValue(text: lastMaxContent, selection: actualSelection, composing: fixRange);
  }

三、文案剪切出现崩溃。原因是我通过substring()方法来剪切,众所周知表情都是由多个字符拼接而成的表达式,如果刚好最后一个是表情,我把其中表情一个字符剪切掉了,导致无法正常显示表情符号,自然会崩溃。
出问题代码(_initOldDataSelection方法):

void _resetSelection(String newText) {
    String result = newText.substring(0, widget.selfMaxLength);
    ……
  }

修复的代码:

void _resetSelection(String newText) {
    var sRunes = newText.runes;
    String result;
    for (int i = 0; i < sRunes.length; i++) {
      if (String.fromCharCodes(sRunes, 0, sRunes.length - i).length <= widget.selfMaxLength) {
        result = String.fromCharCodes(sRunes, 0, sRunes.length - i);
        break;
      }
    }
    ……
  }

四、在第一次达到最大长度时候,出现数组越界的情况。这个也是因为没有正确处理好TextRange的start和end,直接无脑用native层返回的start和end。所以裁切后的text的TextRange我们要根据实际长度定义end值。
出问题代码(_resetSelection方法):

void _resetSelection(String newText) {
   ……
    widget.controller.value = TextEditingValue(text: result, selection: temp, composing: widget.controller.value.composing);
    lastMaxContent = result;
  }

修复的代码:

void _resetSelection(String newText) {
    ……
    //result最后展示在屏幕上的文案
    TextRange fixRange = widget.controller.value.composing;
    if (widget.controller.value.composing.end > result.length) {
      fixRange = TextRange(start: fixRange.start, end: result.length);
    }
    widget.controller.value = TextEditingValue(text: result, selection: temp, composing: fixRange);
    lastMaxContent = result;
  }

五、切换输入法时候,文案恢复修改前。这个属于业务层用法出错,每次build都重新生成一个textController,导致源码修改的controller的text,每次build都被丢掉了。

错误方法:给一个TextField的textController每次一build都新建一个新的controller。

@override
Widget build(BuildContext context) {
return Container(
child: TextField(
……
          controller: TextEditingController.fromValue(TextEditingValue(
          text: _txtController.text,
          selection: TextSelection.fromPosition(
                  TextPosition(affinity: TextAffinity.downstream, offset: _txtController.text.length)))),
          )
       )
}


正确方法:
@override
void initState() {
super.initState();
        _txtController.text = widget.defaultName;
        _txtController.value = TextEditingValue(
                    text: _txtController.text,
                    selection: TextSelection.fromPosition(TextPosition(affinity: TextAffinity.downstream, offset:                   _txtController.text.length)));
}

@override
Widget build(BuildContext context) {
return Container(
            child: TextField(
                inputFormatters: [
                        MTLengthLimitingTextInputFormatter(15),
                ],
            ……
            )
        )
}

总结

  1. flutter官网确实有很多存在的问题,但这些问题正为我们提供研究源码的动力。

相关文章

网友评论

      本文标题:Flutter-输入框潜在bug

      本文链接:https://www.haomeiwen.com/subject/wgrjwhtx.html