(十四)InputField逻辑分析

1.前言

本文来讲一下InputField实现,本文之所以叫逻辑分析是因为跟作者自己和解了,不再去追求一些细节的实现(其实是脑细胞不够理解不了)。

2.获取文字

文字获取通过TouchScreenKeyboard获取。在激活模块时根据需求(比如是否多行,是否只有数字)打开所有键盘,此步操作主要针对触屏设备(苹果、android或者触屏windows)。然后在LateUpdate通过TouchScreenKeyboard.text获取输入文本;通过Input.compositionString获取当前输入(比如汉字是通过字母组合产生,此时获取的为字符)纯粹的字符输入。
在LateUpdate中获取到字符时会进行有效性检查,如下所示:

       // Doesn't include dot and @ on purpose! See usage for details.
        const string kEmailSpecialCharacters = "!#$%&'*+-/=?^_`{|}~";
        
        protected char Validate(string text, int pos, char ch)
        {
            // Validation is disabled
            if (characterValidation == CharacterValidation.None || !enabled)
                return ch;

            if (characterValidation == CharacterValidation.Integer || characterValidation == CharacterValidation.Decimal)
            {
                // Integer and decimal
                bool cursorBeforeDash = (pos == 0 && text.Length > 0 && text[0] == '-');
                bool dashInSelection = text.Length > 0 && text[0] == '-' && ((caretPositionInternal == 0 && caretSelectPositionInternal > 0) || (caretSelectPositionInternal == 0 && caretPositionInternal > 0));
                bool selectionAtStart = caretPositionInternal == 0 || caretSelectPositionInternal == 0;
                if (!cursorBeforeDash || dashInSelection)
                {
                    if (ch >= '0' && ch <= '9') return ch;
                    if (ch == '-' && (pos == 0 || selectionAtStart)) return ch;
                    if (ch == '.' && characterValidation == CharacterValidation.Decimal && !text.Contains(".")) return ch;
                }
            }
            else if (characterValidation == CharacterValidation.Alphanumeric)
            {
                // All alphanumeric characters
                if (ch >= 'A' && ch <= 'Z') return ch;
                if (ch >= 'a' && ch <= 'z') return ch;
                if (ch >= '0' && ch <= '9') return ch;
            }
            else if (characterValidation == CharacterValidation.Name)
            {
                if (char.IsLetter(ch))
                {
                    // Character following a space should be in uppercase.
                    if (char.IsLower(ch) && ((pos == 0) || (text[pos - 1] == ' ')))
                    {
                        return char.ToUpper(ch);
                    }

                    // Character not following a space or an apostrophe should be in lowercase.
                    if (char.IsUpper(ch) && (pos > 0) && (text[pos - 1] != ' ') && (text[pos - 1] != '\''))
                    {
                        return char.ToLower(ch);
                    }

                    return ch;
                }

                if (ch == '\'')
                {
                    // Don't allow more than one apostrophe
                    if (!text.Contains("'"))
                        // Don't allow consecutive spaces and apostrophes.
                        if (!(((pos > 0) && ((text[pos - 1] == ' ') || (text[pos - 1] == '\''))) ||
                              ((pos < text.Length) && ((text[pos] == ' ') || (text[pos] == '\'')))))
                            return ch;
                }

                if (ch == ' ')
                {
                    // Don't allow consecutive spaces and apostrophes.
                    if (!(((pos > 0) && ((text[pos - 1] == ' ') || (text[pos - 1] == '\''))) ||
                          ((pos < text.Length) && ((text[pos] == ' ') || (text[pos] == '\'')))))
                        return ch;
                }
            }
            else if (characterValidation == CharacterValidation.EmailAddress)
            {
                if (ch >= 'A' && ch <= 'Z') return ch;
                if (ch >= 'a' && ch <= 'z') return ch;
                if (ch >= '0' && ch <= '9') return ch;
                if (ch == '@' && text.IndexOf('@') == -1) return ch;
                if (kEmailSpecialCharacters.IndexOf(ch) != -1) return ch;
                if (ch == '.')
                {
                    char lastChar = (text.Length > 0) ? text[Mathf.Clamp(pos, 0, text.Length - 1)] : ' ';
                    char nextChar = (text.Length > 0) ? text[Mathf.Clamp(pos + 1, 0, text.Length - 1)] : '\n';
                    if (lastChar != '.' && nextChar != '.')
                        return ch;
                }
            }
            return (char)0;
        }

3.光标设置

InputField作为输入框还需要显示闪烁的光标。且在pc端还可以通过鼠标拖拽,在移动端通过长按进行多文字选择,此时选装的文字背景要高亮。光标和高亮的实现比较简单但是确定光标的位置或者确定选择的范围比较复杂。

3.1 状态机模式

确定高亮范围和光标闪烁更像是一种状态机模式,即在各种情况下只需要设置caretPositionInternal和caretSelectPositionInternal两个变量即可,后续只需要根据这两个变量进行绘制。这两个变量分别为光标位置和光标选择的位置(此位置是指字符位置)。
比如当外部整体给InputField赋值时,则会整体设置,如下所示:

m_CaretPosition = m_CaretSelectPosition = m_Text.Length;

其他情况则是在处理插入、剪切、选择时等各种条件下caretPositionInternal和caretSelectPositionInternal的值(有时并不会直接设置此两个值,而是设置m_CaretPosition 和m_CaretSelectPosition )。

3.2 光标渲染

光标渲染步骤如下:
1)通过事件系统获取的eventData坐标,转化到组件的局部坐标系下,并通过计算得到光标的字符位置。计算工程比较复杂,简单可以概括为:通过TextGenerator,根据Text的设置以及每个字符的位置,获取光标的字符位置(这里只是简单叙述一下,实际计算中比较复杂)。然后通过int adjustedPos = Mathf.Max(0, caretPositionInternal - m_DrawStart)计算调整后的字符位置
2)在UpdateGeometry中根据Text的TextGenerator计算当前字符对应的x坐标,y坐标则根据字符所在的行数进行计算(也可以根据UICharInfo的y值计算),然后再根据行的高度以及定义的光标宽度进行四个顶点的计算,并基于四个顶点生成mesh。
但此时UpdateGeometry更新的不是自身的mesh(本身也没有),而是根据Canvas生命周期,更新的光标的mesh。为了保证正确显示,需要将光标Caret的RectTransform属性与Text保持一致。

3.3 高亮渲染

选中文字背景高亮实现步骤如下:
1)首先计算得到高亮部分文字的开始和终点索引:

            int startChar = Mathf.Max(0, caretPositionInternal - m_DrawStart);
            int endChar = Mathf.Max(0, caretSelectPositionInternal - m_DrawStart);

2)通过循环依次根据每个字符所在的行,按行最后一个选中的位置以及每行初始字符生成四个顶点,并基于四个顶点生成顶点,并添加三角形设置。如果是单行则要简单的多了。

4.总结

InputField代码3000行(不同的版本不同,但逻辑相同),基本逻辑比较简单如下:
1)通过键盘获取文字,做有效性检查后赋值给Text组件显示。
2)根据当前情况判断是只显示光标还是选择文字。
3)如果是显示光标,则根据光标位置生成光标的mesh
4)如果显示文字选中的高亮,则生成高亮背景的mesh

但麻烦的地方在于要判断多种情况下的选择以及移动平台(更确切为触屏平台)和pc平台的通用处理。比如pc平台通过drag事件确定选择的文字,而移动平台则可以通过键盘的selection方法获得。

5.结语

InputField组件为了处理各种情况,所以会比较臃肿,如果只是实现光标的则比较简单,此处相当多年以前使用笨方法实现的光标,不过此方法不是更新的mesh,是直接显示隐藏UI。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值