zoukankan      html  css  js  c++  java
  • Unity3D ZFBrowser (EmbeddedBrowser) 插件嵌入网页无法输入中文问题

      网页嵌入插件最好的应该就是ZFBrowser了, 可是使用起来也是问题多多, 现在最要命的是网页输入不能打中文, 作者也没打算接入IME, 只能自己想办法了...

      搞了半天只想到一个办法, 就是通过Unity的IME去触发中文输入, 然后传入网页, 也就是说做一个透明的 InputField 盖住网页的输入文本框, 然后在 Update 或是 onValueChanged 中把内容传给网页, 这样基本就能实现中文输入了.

      因为对前端不熟悉, 我就做了一个简单网页做测试:

    <html>
    
    <head>
        <title>My first page</title>
        <style>
            body {
                margin: 0;
            }
        </style>
    </head>
    
    <body>
        <h1>Test Input</h1>
        Field1:    <input type="text" id="field1"> 
        Field2:    <input type="text" id="field2"> 
        <br>
        <br>
        <script>
            function SetInputValue(id, str) {
                document.getElementById(id).value = str;
            }
            function SubmitInput(str)
            {
                document.getElementById("field2").value = "Submited : " + str;
            }
        </script>
    
    </body>
    
    </html>

      这里网页有两个Text Area, 左边作为输入, 右边作为回车后的调用测试:

      然后Unity中直接用一个InputField放到 Field1 的位置上, 设置为透明, 通过Browser类提供的CallFunction方式调用就可以了:

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    using UnityEngine.EventSystems;
    
    namespace UIModules.UITools
    {
        public class BrowserInputField : MonoBehaviour
        {
            [SerializeField]
            public ZenFulcrum.EmbeddedBrowser.Browser browser;
            [SerializeField]
            public InputField input;
    
            [Space(10.0f)]
            [Header("设置网页 input 函数名称")]
            [SerializeField]
            public string SetInputFuncName = "SetInputFuncName";
            [Header("设置网页 submit 函数名称")]
            [SerializeField]
            public string SubmitFuncName = "SubmitFuncName";
            [Header("网页 input id")]
            [SerializeField]
            public string InputElementID = "InputElementID";
    
            public bool inited { get; private set; }
    
            private void Awake()
            {
                this.RequireComponent<CanvasGroup>().alpha = 0.01f;
                Init();
            }
    
            public void Init()
            {
                if(input && (false == inited))
                {
                    inited = true;
                    input.RequireComponent<IME_InputFollower>();       // IME 跟随
    
                    StartCoroutine(CaretAccess((_caret) =>
                    {
                        if(_caret)
                        {
                            var group = _caret.RequireComponent<CanvasGroup>();
                            group.alpha = 1f;
                            group.ignoreParentGroups = true;
                        }
                    }));
                }
            }
    
            IEnumerator CaretAccess(System.Action<Transform> access)
            {
                if(input)
                {
                    var caret = input.transform.Find("InputField Input Caret");
                    while(caret == false && input)
                    {
                        caret = input.transform.Find("InputField Input Caret");
                        yield return null;
                    }
                    access.Invoke(caret);
                }
            }
    
            void Update()
            {
                if(browser && input)
                {
                    browser.CallFunction(SetInputFuncName, new ZenFulcrum.EmbeddedBrowser.JSONNode[2]
                    {
                        new ZenFulcrum.EmbeddedBrowser.JSONNode(InputElementID),
                        new ZenFulcrum.EmbeddedBrowser.JSONNode(input.isFocused ? input.text : (string.IsNullOrEmpty(input.text)?input.placeholder.GetComponent<Text>().text: input.text))
                    });
                }
            }
        }
    }

      这里InputField它会自动生成 Caret 就是输入标记, 为了让他能显示出来, 需要等待到它创建出来之后设置透明度即可. 这里省掉了IME输入法跟随的代码, 那是其它功能了.

      恩, 因为字体大小不一样, 所以Caret位置不准确, 反正是能输入了.

      这是静态的写法, 可以手动去摆 InputField, 可是在很多情况下是不适用的, 比如 Scroll View 里面的元素, 就需要动态去获取了, 可是由于我们无法计算出网页 input 的位置, 所以没法动态地去设置一个InputField来对上网页, 如果对输入标记没有要求的话 (就是那个打字时候会闪的 "|" 竖杠) , 就可以通过注册网页 input 的 onFocus 方法, 来 focus 一个 InputField, 从而触发输入法, 然后再像上面一样监测输入就行了, 而且不需要在网页端写输入函数来调用了, 这个函数我们应该也是可以自己注册进去的...

      这个想法很好, 来测试看看能不能获取网页中的所有 input 节点吧, 在网页那边写测试(因为我确实没写过网页...) : 

    // ... 省略代码了
        Field1: <input type="text" id="field1">
        Field2: <input type="text" id="field2">
        <button type="button" onclick="Click()">Get ID</button>
        <br>
        <p id="show">Show</p>
        <script>
            function Click() {
                var inputs, index;
                var showInfo = "";
                inputs = document.getElementsByTagName('input');
                for (index = 0; index < inputs.length; ++index) {
                    showInfo = showInfo + inputs[index].id + " ";
                }
                document.getElementById("show").innerHTML = showInfo;
            }
        </script>
    // ...

      没错是能获取ID, 那我从Unity那边来添加这个函数试试:

        private void OnGUI()
        {
            if(GUI.Button(new Rect(500, 100, 100, 50), "TestClick"))
            {
                var script = @"
    function TestClick() {
        var inputs, index;
        var array = new Array();
        inputs = document.getElementsByTagName('input');
        for (index = 0; index < inputs.length; ++index) {
            array.push(inputs[index].id);
        }
        return array;
    }";
                var inputs = browser.EvalJS(script);
                if(inputs != null)
                {
                    inputs.Done((_value) =>
                    {
                        if(_value != null)
                        {
                            Debug.Log(_value.ToString());
                        }
    
                        var retVal = browser.CallFunction("TestClick");
                        if(retVal != null)
                        {
                            retVal.Done((_ret) =>
                            {
                                if(_ret != null)
                                {
                                    Debug.Log(_ret.ToString());
                                }
                            });
                        }
                    });
                }
            }
        }

      我创建了一个 TestClick 方法, 通过 EvalJS 解释到网页中, 还好这些解释语言的套路都差不多, 只是不知道它给我返回的是啥, 第一个解释 js function的返回有点意外, 居然是个空 :

      不过没关系, 后面的函数调用返回是我要的 : 

      不错, 返回了我要的节点名称, 这样函数就注册进去然后调用成功了, 说明确实可以通过注入式的代码完成调用, 然后我只需要把另一个设置 input 内容的代码注入进去, 就可以随时修改所有 input 对象了.

    // 本作核心代码
        function SetInputValue(id, str) {
            document.getElementById(id).value = str;
        }

      马上加进去看看, 先整合一下代码把请求提取出来 : 

        public static void WebBrowserFunctionRegister(ZenFulcrum.EmbeddedBrowser.Browser browser, string function, System.Action<ZenFulcrum.EmbeddedBrowser.JSONNode> succ = null)
        {
            if(browser)
            {
                var register = browser.EvalJS(function);
                if(register != null)
                {
                    register.Done((_value) =>
                    {
                        if(succ != null)
                        {
                            succ.Invoke(_value);
                        }
                    });
                }
            }
        }
        public static void WebBrowserFunctionCall(ZenFulcrum.EmbeddedBrowser.Browser browser, string functionname, ZenFulcrum.EmbeddedBrowser.JSONNode[] param,
            System.Action<ZenFulcrum.EmbeddedBrowser.JSONNode> result = null)
        {
            if(browser)
            {
                var retVal = param != null && param.Length > 0 ? browser.CallFunction(functionname, param) : browser.CallFunction(functionname);
                if(retVal != null)
                {
                    retVal.Done((_ret) =>
                    {
                        if(result != null)
                        {
                            result.Invoke(_ret);
                        }
                    });
                }
            }
        }
    
        private void OnGUI()
        {
            if(GUI.Button(new Rect(500, 100, 100, 50), "TestClick"))
            {
                var testClick = @"
    function TestClick() {
        var inputs, index;
        var array = new Array();
        inputs = document.getElementsByTagName('input');
        for (index = 0; index < inputs.length; ++index) {
            array.push(inputs[index].id);
        }
        return array;
    }";
                var coreScript = @"
        function SetInputValue(id, str) {
            document.getElementById(id).value = str;
        }
    ";
                WebBrowserFunctionRegister(browser, testClick, (_) =>
                {
                    WebBrowserFunctionCall(browser, "TestClick", null, (_ret) =>
                    {
                        WebBrowserFunctionRegister(browser, coreScript, (__) =>
                        {
                            var list = LitJson.JsonMapper.ToObject<List<string>>(_ret.AsJSON);
                            if(list != null)
                            {
                                foreach(var id in list)
                                {
                                    WebBrowserFunctionCall(browser, "SetInputValue", new ZenFulcrum.EmbeddedBrowser.JSONNode[2] {
                                        new ZenFulcrum.EmbeddedBrowser.JSONNode(id),
                                        new ZenFulcrum.EmbeddedBrowser.JSONNode("测试:" + id),
                                    });
                                }
                            }
                        });
                    });
                });
            }
        }

      因为我不确定它是不是都是异步的, 所以都用回调的形式来做了, 结果喜人, 确实能够正确运行了:

      几乎成了, 下一步就是注册一下 input 的 focus 事件, 在网页触发 focus 之后就创建一个 InputField 按照套路走就行了, 在 InputField 的focus取消的时候销毁它, 就能完美解决输入法问题了...

     -------------------------------------------------------------------------------------------------

    (2020.7.7)

      之前的理论没有问题, 不过可以更加简化一点, 首先 ZFBrowser 解析的网页, 它的 focus 跟 InputField 中的 focus 并不冲突, 并且在两边都 focus 的情况下, 网页接收的输入就是 Unity 调用的 IME, 所以就不需要同步 InputField 中的输入到网页那边了, InputField 只作为启动 IME 的入口即可.

      然后发现很多网页中的 input 元素并不使用 id, 而是直接 class 设置了调用逻辑, 比较面向过程, 而且W3C标准中, 每个控件或者元素, 并没有一个GUID, 这就无法通过唯一ID定位到某个元素上了 ( [对Web页面元素的绝对唯一引用方法] https://www.cnblogs.com/birdshome/archive/2006/09/28/uniqueid_usage.html )...

      那么我们想要获取和设置某个 input 的元素的时候, 就需要自己给没有 id 的 input 元素添加ID了.

      然后一个元素的调用函数, 不像C#中的delegate那么方便, 你要添加一个唯一调用, 只需要删除原有回调再添加即可:

        browser.onConsoleMessage -= OnConsoleMessage;
        browser.onConsoleMessage += OnConsoleMessage;

      C#怎么样都不会错误添加多个同样的回调. 可是JS没有这个, 有些人自己写了相似的, 可是不是面向对象, 肯定会出错.

      首先来看看怎样给 input 元素添加 id, 然后添加 onfocus 方法给它, 让它在焦点的时候能够通知到 Unity 来创建 InputField 触发 IME.

      /* 创建唯一ID代码 */

        public const string InjectInputID_JS_Name = "InjectInputID";
        public const string InjectInputID_JS = @"
    var inputID = 1;
    function InjectInputID() {
        var inputs, index;
        inputs = document.getElementsByTagName('input');
        for (index = 0; index < inputs.length; ++index) {
            var rawID = inputs[index].id;
            if(rawID == null || rawID == ''){
                inputs[index].id = 'custom_input_id_' + (inputID++);
            }
        }
    }";
    
    // 某处调用注册函数
    WebBrowserFunctionRegister(browser, InjectInputID_JS);

      上面的注入ID代码使用了一个全局变量 inputID, 这样在设置时就能避免网页动态加载出来的新元素得到同样的ID了.

      /* 添加回调事件方法 */

        public const string AddEventFunc_JS_Name = @"AddEventFunc";
        public const string eventFuncNameTemplate_JS = "EVENTFUNC";
        public const string eventTemplateName_JS = "EVENTNAME";
        public const string AddEventFuncTemplate_JS = @"
    function AddEventFunc(elementID) 
    {
        var tagElement = document.getElementById(elementID);
        if (tagElement != null) 
        {
            var oldFuncStr = (tagElement.EVENTFUNC + '').replace(/(
    )+|(
    )+/g, '');
            var rawFunc = oldFuncStr.substring(oldFuncStr.indexOf('{') + 1, oldFuncStr.indexOf('}'));
            var newFunc = function() 
            {
                eval(rawFunc);
                console.log(elementID + ':EVENTNAME');
            }
            if((newFunc + '').replace(/(
    )+|(
    )+/g, '') == oldFuncStr){
                return;
            }
            tagElement.EVENTFUNC = newFunc;
        }
    }";
    
        public enum ElementEventFunc
        {
            onclick,
            onsubmit,
            onfocus
        }
        public static string GenerateAddEventFunc_JS_Code(ElementEventFunc eventFunc, string customEvent)
        {
            return GenerateAddEventFunc_JS_Code(eventFunc.ToString(), customEvent);
        }
        public static string GenerateAddEventFunc_JS_Code(string eventFunc, string customEvent)
        {
            return AddEventFuncTemplate_JS.Replace(eventFuncNameTemplate_JS, eventFunc).Replace(eventTemplateName_JS, customEvent);
        }
    
    // 某处调用注册函数
        string focusFunc_JS = GenerateAddEventFunc_JS_Code(ElementEventFunc.onfocus, ElementEventFunc.onfocus.ToString() + ":" + browser.GetHashCode().ToString());
        WebBrowserFunctionRegister(browser, focusFunc_JS);

      这里使用了一个模板来创建 function, 因为考虑到以后可能会使用到其它事件的注册, 不一定只有 focus 的.

      说来浏览器的解释执行代码也挺神奇的, 一个函数可以直接以字符串的方式获取, 好像叫Blob, 反正就像上面代码中的, 比如是 onfocus 函数, 那么就成了:

    var oldFuncStr = (tagElement.onfocus + '').replace(/(
    )+|(
    )+/g, '');

      这样就把 onfocus 的调用方法字符串得到了, 像是下面这样:

    <input type="text" id="field1", onfocus="OnFocus(this.id)">
    <script>
        function OnFocus(id){
            console.log(id);
        }
        function Test() {
            var tag  = document.getElementById('field1');
            console.log((tag.onfocus + '').replace(/(
    )+|(
    )+/g, ''));
        }
        Test() 
    </script> 

      得到的 onfocus 字符串 : 

    function onfocus(event) {  OnFocus(this.id)}

      然后就是把里面的方法取出来, 封装到新的方法里面去, 当然原有方法是字符串, 必须使用 eval 来进行编译调用:

        var rawFunc = oldFuncStr.substring(oldFuncStr.indexOf('{') + 1, oldFuncStr.indexOf('}'));
        var newFunc = function() 
        {
            eval(rawFunc);
            console.log(elementID + ':onfocus:XXXX');    // 这里运行时XXXX被设置为unity Browser对象的哈希值
        }
        if((newFunc + '').replace(/(
    )+|(
    )+/g, '') == oldFuncStr){
            return;
        }
        tagElement.onfocus = newFunc;                    // 

      newFunc 就包含了老函数调用和新的 Log, 我们就是以监听 log 来发送消息的, 中间有个比较 newFunc 和 oldFuncStr 的逻辑, 因为它不像delegate那样可以不重复添加回调, 并且 JS 的回调会包含闭包信息之类的, 如果这个添加回调的添加了两次, 它会造成死循环, 我不是很清楚为什么, 所以判断相同的回调时不再进行添加. 这里就限定了只能添加一次回调, 逻辑是有问题的, 不过本工程中使用上已经够了.  

      这样就能注册并监听网页 input 元素的 onfocus 事件了. 详细注册方法如下, 因为网页回调的log必须带有对应的网页ID, 才能分清是哪个网页的 input 被焦点了:

        private Dictionary<Browser, HashSet<string>> m_focusTargets = new Dictionary<Browser, HashSet<string>>();
        
        public const string GetInputs_JS_Name = "GetInputs";
        public const string GetInputs_JS = @"
    function GetInputs() {
        var inputs, index;
        var array = new Array();
        inputs = document.getElementsByTagName('input');
        for (index = 0; index < inputs.length; ++index) {
            array.push(inputs[index].id);
        }
        return array;
    }";
        
        private void RegisterBaseFunctions(Browser browser)
        {
            browser.onConsoleMessage -= OnFocus;
            browser.onConsoleMessage += OnFocus;
    
            WebBrowserFunctionRegister(browser, InjectInputID_JS);
            string focusFunc_JS = GenerateAddEventFunc_JS_Code(ElementEventFunc.onfocus, ElementEventFunc.onfocus.ToString() + ":" + browser.GetHashCode().ToString());
            WebBrowserFunctionRegister(browser, GetInputs_JS);
        }
        
        private void OnFocus(string msg, string src)
        {
            if(string.IsNullOrEmpty(msg))
            {
                return;
            }
            Debug.Log("OnFocus msg : " + msg);
            var sp = msg.Split(':');
            if(sp != null && sp.Length >= 3)
            {
                var id = sp[0];
                var hashCode = sp[2];
                switch(sp[1])
                {
                    case "onfocus":
                        {
                            OnFocus(GetBrowserByHash(hashCode), id);
                        }
                        break;
                }
            }
        }
        
        public Browser GetBrowserByHash(string hashCode)
        {
            foreach(var browser in m_scanTargets.Keys)
            {
                if(browser && string.Equals(browser.GetHashCode().ToString(), hashCode, System.StringComparison.Ordinal))
                {
                    return browser;
                }
            }
            return null;
        }
        
        private void OnFocus(Browser browser, string id, string text = null)
        {
            if(browser)
            {
                // ......
            }
        }    

      因为网页会动态加载或者创建元素, 所以获取 input 和注入回调需要在update或者协程中不断地获取, 来保证每个 input 的回调正确...

      如果 input 的 id 是 "field1", 那么回调中传回来的 message 就是 "field1:onfocus:XXXX" ,  XXXX就是 browser.GetHashCode().ToString() 

      在协程中去不断检测是否有新 input 元素:

        // 在某处调用
        StartCoroutine(CheckWebInput());
        
        private IEnumerator CheckWebInput()
        {
            while(true)
            {
                yield return null;
                foreach(var tags in m_focusTargets)
                {
                    var browser = tags.Key;
                    BrowserInputCheck(browser, tags.Value);
                }
            }
        }    
            
        // 因为网页可能动态加载, 我们需要不断地获取网页 input 元素, 来进行注册 onfocus 回调
        private void BrowserInputCheck(Browser browser, HashSet<string> exists)
        {
            if(browser && browser.IsLoaded)
            {
                WebBrowserFunctionCall(browser, InjectInputID_JS_Name, null, (_) =>
                {
                    WebBrowserFunctionCall(browser, GetInputs_JS_Name, null, (_ret) =>
                    {
                        var inputIDs = LitJson.JsonMapper.ToObject<List<string>>(_ret.AsJSON);
                        if(inputIDs != null && inputIDs.Count > 0)
                        {
                            foreach(var inputID in inputIDs)
                            {
                                if(exists.Contains(inputID) == false)
                                {
                                    exists.Add(inputID);
                                    WebBrowserFunctionCall(browser, AddEventFunc_JS_Name, new JSONNode[1] { new JSONNode(inputID) });
                                }
                            }
                        }
                    });
                });
            }
        }

      当可以正常收到 onfocus 事件之后, 只需要对应创建 InputField 组件, 然后同样把 Unity 的 Focus 给这个 InputField 就行了, 至于 html 的 blur (丢失焦点) 事件, 这里是不用监听的, 因为 InputField 同样会因为鼠标操作, 键盘 Enter/Return 按键, ESC 按键触发 OnEditEnd 并丢失焦点, 所以我们只需要监听没有丢失焦点的情况即可.

      在什么情况下 Unity的 InputField会丢失焦点而网页 input 不会丢失焦点呢? 测试了一下包含以下情形:

      1. Enter / Return / ESC 键盘都触发了 InputField 的丢失焦点, 可是网页并不一定会丢失焦点.

      2. 鼠标移出网页显示的UI区域, 网页会丢失焦点, 不过鼠标移回网页它会自动触发 onfocus, 这都没有问题, 可是如果用户再次点击网页 input 区域, 不会再次触发 onfocus 事件, 此时由于点击操作 InputField 会丢失焦点.

      应对这些情况, 就需要做对应修改, 在 InputField 的 onEndEdit 回调中, 添加相关测试以及操作 (回调经过封装处理, 变量 BrowserInputField 包含了相关组件引用) : 

        public const string HasFocusFunc_JS_Name = "GetHasFocus";
        public const string HasFocusFunc_JS = @"
    function GetHasFocus(id) {
        var target = document.getElementById(id);
        return (target != null && target.id == document.activeElement.id);
    }";
        public const string CancelFocus_JS_Name = "CancelFocus";
        public const string CancelFocus_JS = @"
    function CancelFocus() {
        document.activeElement.blur();
    }";
        
        // 在某处进行注册
        WebBrowserFunctionRegister(browser, HasFocusFunc_JS);
        WebBrowserFunctionRegister(browser, CancelFocus_JS);
        
        // 封装后的 InputField.onEndEdit 回调. BrowserInputField 包含相关组件
        private void OnEditEnd(BrowserInputField inputField)
        {
            if(inputField)
            {
                if(Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter) || Input.GetKeyDown(KeyCode.Escape))
                {
                    WebBrowserFunctionCall(inputField.browser, CancelFocus_JS_Name, null);
                }
                else
                {
                    var ui = MathTools.GetMouseOnUI();
                    if(ui)
                    {
                        var guiData = ui.GetComponent<RuntimeData.BrowserGUINetData>();
                        if(guiData && guiData.browser == inputField.browser)
                        {
                            Core.CoroutineRoot.instance.RunWaitFrames(2, () =>
                            {
                                WebBrowserFunctionCall(inputField.browser, HasFocusFunc_JS_Name, new JSONNode[1] { new JSONNode(inputField.InputElementID) }, (_ret) =>
                                {
                                    Common.DataTable value = _ret.AsJSON;
                                    if((bool)value)
                                    {
                                        inputField.FocusInputField();
                                    }
                                });
                            });
                        }
                    }
                }
            }
        }

      1. 当Enter / Return / ESC 键盘都触发了 InputField 的丢失焦点, 强行对网页当前的焦点执行 blur 操作, 禁止没有IME的输入继续输入网页.

      2. 当鼠标还在网页UI区域时, 如果丢失了焦点就检测对应ID的 input 是否也丢失了焦点, 如果没有就重新焦点到 InputField. 

      这样解决了焦点问题之后, 输入 中文 / 日文 这些需要IME支持的语言就能正确输入了, 当然如果看了源代码知道怎样直接触发IME的话, 可以把 InputField 省略掉. Unity 怎样Focus目标的代码:

            public void FocusInputField()
            {
                if(input)
                {
                    if (EventSystem.current.currentSelectedGameObject != input.gameObject)
                    {
                        EventSystem.current.SetSelectedGameObject(input.gameObject, null);
                    }            
                    input.OnPointerClick(new PointerEventData(EventSystem.current));
                }
            }

      IME 跟随在之前的帖子里 : https://www.cnblogs.com/tiancaiwrk/p/12603955.html

      完成这些之后, 还有一步, 就是动态创建 InputField 以及让它跟随 input 元素的位置, 因为 IME 会跟随 InputField, 可是 InputField 怎样跟随网页 input 元素呢? 这里当然地需要获取 input 元素的位置信息了, html 提供了相关代码 : 

        // 获取页面大小
        var width = document.body.clientWidth;
        var height = document.body.clientHeight;
        // 获取元素位置以及大小
        var tag = document.getElementById(id);
        var rect = tag.getBoundingClientRect();

      页面大小可以理解为整个 html 渲染区域的大小, 它跟UGUI的大小有对应关系, 比如下图显示页面大小1000x1000, 在UGUI上它渲染的大小就是UI的Rect Size:

      这需要关闭自动修改尺寸才能实现:

      如果是这种情况, 那么计算 input 元素的位置就需要经过二次转换了, 首先UGUI中屏幕位置左下角为(0,0), 而 html 中左上角才是(0,0):

      1. 需要先获取页面大小, 然后获取 input 在页面中的位置

      2. 获取渲染UI的大小, 并获取UI左上角的坐标位置

      3. 计算 input 在页面中的归一化位置 [0,1], 然后相对UI左上角位置获取偏移量, 并转换到当前UI Pivot的相对偏移量

      4. UI位置加上偏移量就是 input 元素的位置了, 可以在Update中进行跟随操作 (这是网页渲染到UI面板, 面板可移动的情况)

      5. 如果页面内的元素也是可以移动的, 比如页面中有Scroll可以滑动input, 就需要随时重新计算偏移量了

      以上面的触发Focus的位置作为入口, 创建InputField, 并计算跟随偏移量 : 

        const string GetElementRect_JS_Name = "GetElementRect";
        const string GetElementRect_JS = @"
    function GetElementRect(id)
    {
        var tag = document.getElementById(id);
        var rect = tag.getBoundingClientRect();
        var region = {};
        region['x'] = rect.left;
        region['y'] = rect.top;
        region['width'] = Math.abs(rect.right - rect.left);
        region['height'] = Math.abs(rect.top - rect.bottom);
        return region;
    }";
    
        const string GetHtmlBodySize_JS_Name = "GetHtmlBodySize";
        const string GetHtmlBodySize_JS = @"
    function GetHtmlBodySize()
    {    
        var region = {};
        region['x'] = document.body.clientWidth;
        region['y'] = document.body.clientHeight;
        return region;
    }";
        
        // 在某处注册代码
        WebBrowserFunctionRegister(browser, GetElementRect_JS);
        WebBrowserFunctionRegister(browser, GetHtmlBodySize_JS);
        
        private void OnFocus(Browser browser, string id, string text = null)
        {
            if(browser)
            {
                var dict = m_scanTargets.GetValue(browser);
                var browserInputField = dict.TryGetNullableValue(id);
                if(browserInputField == false)
                {
                    // 创建代码, 省略
                }
                if(browserInputField)
                {
                    InputFieldFollowWebInput(browserInputField);    // 设置跟随
                    browserInputField.gameObject.SetActive(true);
                    browserInputField.FocusInputField();            // Focus 到InputField, 触发IME
                }
            }
        }
        
        private void InputFieldFollowWebInput(BrowserInputField inputField)
        {
            if(inputField && inputField.browser)
            {
                inputField.transform.position = Input.mousePosition;
                WebBrowserFunctionCall(inputField.browser, GetElementRect_JS_Name,
                    new ZenFulcrum.EmbeddedBrowser.JSONNode[1] { new ZenFulcrum.EmbeddedBrowser.JSONNode(inputField.InputElementID) }, (_rect) =>
                    {
                        var inputRect = LitJson.JsonMapper.ToObject<Rect>(_rect.AsJSON);
                        WebBrowserFunctionCall(inputField.browser, GetHtmlBodySize_JS_Name, null, (_size) =>
                        {
                            var htmlSize = LitJson.JsonMapper.ToObject<Vector2>(_size.AsJSON);
                            var tag = inputField.browser.GetComponent<Modules.BrowserRenderTarget>();
                            if(tag)
                            {
                                var rect = tag.browserGUINetData.transform as RectTransform;
                                if(rect)
                                {
                                    var uiSize = rect.rect.size;
                                    var anchorPos = rect.position + MathTools.Multiple(new Vector2(0, 1) - rect.pivot, uiSize).UpGrade(MathTools.VecAxis.Z);
                                    var x = (inputRect.x / htmlSize.x) * uiSize.x;
                                    var y = (inputRect.y / htmlSize.y) * uiSize.y;
                                    var inputPos = anchorPos + new Vector3(x, -y, 0);
                                    var offset = MathTools.Multiple(inputPos - rect.position, rect.lossyScale);
                                    Tools.UIObjectFollow.instance.AddFollowInfo(inputField.transform as RectTransform, rect, offset);
                                }
                            }
                        });
                    });
            }
        }

    (2020.07.09)

      上面的逻辑仍旧是对页面元素位置不变而计算的( UGUI可以移动位置, 因为输入法可以跟随 ), 说到页面元素位置会变化的情况, 以最简单的例子为例 : 当界面大小与网页大小不同时, 有 Scroll 拖动条的情况.

      先看看网页代码, 控制了元素大小使得页面大小比较大:

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title>Scroll</title>
        <style>
            #top {
                height: 500px;
                color: #FFF;
                background-color: #000000;
            }
            #bottom {
                height: 100px;
                color: rgb(0, 0, 0);
                background-color: #ffffff;
            }
        </style>
    </head>
    <body>
        <div>
            Input1 :
            <input id="field1">
            <button id="top" onclick="GetInfo()">Button</button>
            <br> Input2 :
            <input id="bottom">
        </div>
    
        <script type="text/javascript">
            function GetInfo() {
                console.log("网页大小 : " + document.body.clientWidth + " x " + document.body.clientHeight);
            }
        </script>
    </body>
    </html>

      那么在Unity中运行时, 有UGUI大小, 以及Browser设定页面大小, 以及实际网页大小:

      ( UGUI 400 x 400 )

      ( Browser 300 x 500 )

      网页Log打印出 267 x 627, 所以在页面上可以看到因为 Browser的大小 Height 500 < 627, 所以出现了拖动条, 而且映射到了UGUI 400 x 400 的RawImage上, 所以产生了缩放.

      因为我制作的 InputField 预制体是以左上角对齐的, 所以只需要计算出网页 input 的左上角坐标即可:

      

      获取 input 在html中坐标的时候, 它给的坐标是已经计算过Scroll之后的坐标, 所以直接使用即可:

      看到它们的 top 位置因为 Scroll 拖动而改变了, 这样就省了我们要去计算 Scroll 造成的偏移了.

      下面的代码就是怎样计算 input 位置到 UGUI 位置的算法了:

        private void InputFieldFollowWebInput(BrowserInputField inputField)
        {
            if(inputField && inputField.browser)
            {
                WebBrowserFunctionCall(inputField.browser, GetElementRect_JS_Name,
                    new ZenFulcrum.EmbeddedBrowser.JSONNode[1] { new ZenFulcrum.EmbeddedBrowser.JSONNode(inputField.InputElementID) }, (_rect) =>
                    {
                        var inputRect = LitJson.JsonMapper.ToObject<Rect>(_rect.AsJSON);
                        InputFieldFollowWebInput(inputField, inputRect);
                    });
            }
        }
        private void InputFieldFollowWebInput(BrowserInputField inputField, Rect inputRect)
        {
            if(inputField && inputField.browser)
            {
                var tag = inputField.browser.GetComponent<Modules.BrowserRenderTarget>();
                if(tag && tag.renderTarget)
                {
                    var rect = tag.renderTarget.rectTransform;
                    if(rect)
                    {
                        var uiSize = rect.rect.size;        // UGUI size
                        var anchorPos = rect.position + MathTools.Multiple(new Vector2(0, 1) - rect.pivot, uiSize).UpGrade(MathTools.VecAxis.Z); // ugui top left pos
                        var browserSize = inputField.browser.Size;  // uiSize equals to how large the browserSize is
    
                        var x_normalized = (inputRect.x / browserSize.x);     // normalized pos_x
                        var y_normalized = (inputRect.y / browserSize.y);     // normalized pos_y
                        var x = (x_normalized) * uiSize.x;
                        var y = (y_normalized) * uiSize.y;
                        var inputPos = anchorPos + new Vector3(x, -y, 0);
                        var offset = MathTools.Multiple(inputPos - rect.position, rect.lossyScale);
                        var inputPosUI = rect.position + offset;
                        inputField.transform.position = inputPosUI;
                    }
                }
            }
        }

      感觉比之前的算法反而更简单了? 没错, 因为使用了统一归一化算法, 本来 x_normalized , y_normalized 需要使用 html 网页大小来进行计算的:

        var x_normalized = (inputRect.x / htmlSize.x) * (htmlSize.x / browserSize.x);  // normalized pos_x
        var y_normalized = (inputRect.y / htmlSize.y) * (htmlSize.y / browserSize.y);  // normalized pos_y

      不过刚好因为统一归一化被消除了, 并且 html 的元素位置信息包含了 Scroll 之后的信息, 所以其他信息都不需要了, 只需要 input 元素的位置信息就够了. 把计算要放在Update之中才行了, 因为要随时计算坐标......

      经过这些过程, 基本上坐标没有问题了, 因为运行时 InputField 是完全透明的, 所以大小之类的都无所谓了, 只要去除 raycast 相关的让人无法选中即可.

     ----------------------- 简化了一些代码 --------------------------

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    using UnityEngine.EventSystems;
    
    namespace UIModules.UITools
    {
        /// <summary>
        /// embered input to browser input, active the IME
        /// </summary>
        public class BrowserInputField : MonoBehaviour
        {
            [SerializeField]
            public ZenFulcrum.EmbeddedBrowser.Browser browser;
            [SerializeField]
            public InputField input;
    
            [Space(10.0f)]
            [Header("网页 input id")]
            [SerializeField]
            public string InputElementID = "InputElementID";
    
            public Core.Event<BrowserInputField> onEditEnd = new Core.Event<BrowserInputField>();
            public bool inited { get; private set; }
    
            #region Mono Funcs
            private void Awake()
            {
                this.RequireComponent<CanvasGroup>().alpha = 0.0f;
                Init();
            }
            #endregion
    
            #region Main Funcs
            public void Init()
            {
                if(input && (false == inited))
                {
                    inited = true;
                    input.RequireComponent<IME_InputFollower>();
                    input.RequireComponent<InputFocusOutOfControl>();
    
                    input.onEndEdit.AddListener(OnEndEdit);
                }
            }
            public void FocusInputField()
            {
                if(input)
                {
                    if (EventSystem.current.currentSelectedGameObject != input.gameObject)
                    {
                        EventSystem.current.SetSelectedGameObject(input.gameObject, null);
                    }            
                    input.OnPointerClick(new PointerEventData(EventSystem.current));
                }
            }
            #endregion
    
            #region Events
            private void OnEndEdit(string txt)
            {
                onEditEnd.Invoke(this);
            }
            #endregion
    
        }
    }
    BrowserInputField
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    using UnityEngine.EventSystems;
    
    namespace UIModules.UITools
    {
        using ZenFulcrum.EmbeddedBrowser;
    
        /// <summary>
        /// AutoBrowserInputField can auto fit html input element
        /// </summary>
        public class AutoBrowserInputField : SingletonComponent<AutoBrowserInputField>
        {
            private Dictionary<Browser, Dictionary<string, BrowserInputField>> m_scanTargets = new Dictionary<Browser, Dictionary<string, BrowserInputField>>();
            private Dictionary<Browser, HashSet<string>> m_focusTargets = new Dictionary<Browser, HashSet<string>>();
    
            public UIModules.UITools.BrowserInputField inputTemplate
            {
                get
                {
                    return UIManager.instance.Get<BrowserInputFieldPanel>("UI/CommonUI/BrowserInputFieldPanel").browserInputFieldTemplate;
                }
            }
    
            #region Overrides
            protected override void Initialize()
            {
            }
            protected override void UnInitialize()
            {
            }
            #endregion
    
            #region Mono Funcs
            private void Update()
            {
                CheckWebInput();
            }
            #endregion
    
            #region Main Funcs
            public void AddAutoInputBrowser(Browser browser)
            {
                if(browser)
                {
                    RemoveAutoInputBrowser(browser);
                    RegisterBaseFunctions(browser);
                }
            }
            public Browser GetBrowserByHash(string hashCode)
            {
                foreach(var browser in m_scanTargets.Keys)
                {
                    if(browser && string.Equals(browser.GetHashCode().ToString(), hashCode, System.StringComparison.Ordinal))
                    {
                        return browser;
                    }
                }
                return null;
            }
    
            public void RemoveAutoInputBrowser(Browser browser)
            {
                if(browser)
                {
                    var dict = m_scanTargets.TryGetNullableValue(browser);
                    if(dict != null)
                    {
                        foreach(var field in dict.Values)
                        {
                            if(field)
                            {
                                GameObject.Destroy(field.gameObject);
                            }
                        }
                        m_scanTargets.Remove(browser);
                    }
                    m_focusTargets.Remove(browser);
                }
            }
    
            private void CheckWebInput()
            {
                foreach(var tags in m_focusTargets)
                {
                    var browser = tags.Key;
                    BrowserInputCheck(browser, tags.Value);
                }
                foreach(var tags in m_scanTargets.Values)
                {
                    foreach(var browserInputField in tags.Values)
                    {
                        InputFieldFollowWebInput(browserInputField);
                    }
                }
            }
            #endregion
    
            #region Events
            private void OnFocus(string msg, string src)
            {
                if(string.IsNullOrEmpty(msg))
                {
                    return;
                }
                Debug.Log("OnFocus msg : " + msg);
                var sp = msg.Split(':');
                if(sp != null && sp.Length >= 3)
                {
                    var id = sp[0];
                    var hashCode = sp[2];
                    switch(sp[1])
                    {
                        case "onfocus":
                            {
                                OnFocus(GetBrowserByHash(hashCode), id);
                            }
                            break;
                    }
                }
            }
            private void OnFocus(Browser browser, string id, string text = null)
            {
                if(browser)
                {
                    var dict = m_scanTargets.GetValue(browser);
                    var browserInputField = dict.TryGetNullableValue(id);
                    if(browserInputField == false)
                    {
                        browserInputField = UIManager.CopyItem<BrowserInputField>(inputTemplate.transform as RectTransform);
                        browserInputField.gameObject.name = id;
                        dict[id] = browserInputField;
    
                        browserInputField.browser = browser;
                        browserInputField.InputElementID = id;
    
                        browserInputField.Init();
    
                        browserInputField.onEditEnd.AddListener(OnEditEnd);
                    }
                    if(browserInputField)
                    {
                        if(browserInputField.input)
                        {
                            browserInputField.input.text = string.Empty;
                        }
                        InputFieldFollowWebInput(browserInputField);
                        browserInputField.gameObject.SetActive(true);
                        browserInputField.FocusInputField();
                    }
                }
            }
            private void OnEditEnd(BrowserInputField inputField)
            {
                if(inputField)
                {
                    if(Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter) || Input.GetKeyDown(KeyCode.Escape))
                    {
                        WebBrowserFunctionCall(inputField.browser, CancelFocus_JS_Name, null);
                    }
                    else
                    {
                        var ui = MathTools.GetMouseOnUI();
                        if(ui)
                        {
                            var guiData = ui.GetComponent<RuntimeData.BrowserGUINetData>();
                            if(guiData && guiData.browser == inputField.browser)
                            {
                                Core.CoroutineRoot.instance.RunWaitFrames(2, () =>
                                {
                                    WebBrowserFunctionCall(inputField.browser, HasFocusFunc_JS_Name, new JSONNode[1] { new JSONNode(inputField.InputElementID) }, (_ret) =>
                                    {
                                        Common.DataTable value = _ret.AsJSON;
                                        if((bool)value)
                                        {
                                            inputField.FocusInputField();
                                        }
                                    });
                                });
                            }
                        }
                    }
                }
            }
            #endregion
    
            #region Help Funcs
            private void RegisterBaseFunctions(Browser browser)
            {
                m_scanTargets[browser] = new Dictionary<string, BrowserInputField>();
                m_focusTargets[browser] = new HashSet<string>();
    
                browser.onConsoleMessage -= OnFocus;
                browser.onConsoleMessage += OnFocus;
    
                WebBrowserFunctionRegister(browser, SetInputValue_JS);
                WebBrowserFunctionRegister(browser, HasFocusFunc_JS);
                WebBrowserFunctionRegister(browser, CancelFocus_JS);
                WebBrowserFunctionRegister(browser, InjectInputID_JS);
                string focusFunc_JS = GenerateAddEventFunc_JS_Code(ElementEventFunc.onfocus, ElementEventFunc.onfocus.ToString() + ":" + browser.GetHashCode().ToString());
                WebBrowserFunctionRegister(browser, focusFunc_JS);
                WebBrowserFunctionRegister(browser, GetInputs_JS);
    
                WebBrowserFunctionRegister(browser, GetElementRect_JS);
            }
            private void BrowserInputCheck(Browser browser, HashSet<string> exists)
            {
                if(browser && browser.IsLoaded)
                {
                    WebBrowserFunctionCall(browser, InjectInputID_JS_Name, null, (_) =>
                    {
                        WebBrowserFunctionCall(browser, GetInputs_JS_Name, null, (_ret) =>
                        {
                            var inputIDs = LitJson.JsonMapper.ToObject<List<string>>(_ret.AsJSON);
                            if(inputIDs != null && inputIDs.Count > 0)
                            {
                                foreach(var inputID in inputIDs)
                                {
                                    if(exists.Contains(inputID) == false)
                                    {
                                        exists.Add(inputID);
                                        WebBrowserFunctionCall(browser, AddEventFunc_JS_Name, new JSONNode[1] { new JSONNode(inputID) });
                                    }
                                }
                            }
                        });
                    });
                }
            }
    
            private void InputFieldFollowWebInput(BrowserInputField inputField)
            {
                if(inputField && inputField.browser)
                {
                    WebBrowserFunctionCall(inputField.browser, GetElementRect_JS_Name,
                        new ZenFulcrum.EmbeddedBrowser.JSONNode[1] { new ZenFulcrum.EmbeddedBrowser.JSONNode(inputField.InputElementID) }, (_rect) =>
                        {
                            var inputRect = LitJson.JsonMapper.ToObject<Rect>(_rect.AsJSON);
                            InputFieldFollowWebInput(inputField, inputRect);
                        });
                }
            }
            private void InputFieldFollowWebInput(BrowserInputField inputField, Rect inputRect)
            {
                if(inputField && inputField.browser)
                {
                    var tag = inputField.browser.GetComponent<Modules.BrowserRenderTarget>();
                    if(tag && tag.renderTarget)
                    {
                        var rect = tag.renderTarget.rectTransform;
                        if(rect)
                        {
                            var uiSize = rect.rect.size;        // UGUI size
                            var anchorPos = rect.position + MathTools.Multiple(new Vector2(0, 1) - rect.pivot, uiSize).UpGrade(MathTools.VecAxis.Z); // ugui top left pos
                            var browserSize = inputField.browser.Size;  // uiSize equals to how large the browserSize is
    
                            var x_normalized = (inputRect.x / browserSize.x);     // normalized pos_x
                            var y_normalized = (inputRect.y / browserSize.y);     // normalized pos_y
                            var x = (x_normalized) * uiSize.x;
                            var y = (y_normalized) * uiSize.y;
                            var inputPos = anchorPos + new Vector3(x, -y, 0);
                            var offset = MathTools.Multiple(inputPos - rect.position, rect.lossyScale);
                            var inputPosUI = rect.position + offset;
                            inputField.transform.position = inputPosUI;
                        }
                    }
                }
            }
    
            public static void WebBrowserFunctionRegister(ZenFulcrum.EmbeddedBrowser.Browser browser, string function,
                System.Action<ZenFulcrum.EmbeddedBrowser.JSONNode> succ = null)
            {
                if(browser)
                {
                    var register = browser.EvalJS(function);
                    if(register != null)
                    {
                        register.Done((_value) =>
                        {
                            if(succ != null)
                            {
                                succ.Invoke(_value);
                            }
                        });
                    }
                }
            }
            public static void WebBrowserFunctionCall(ZenFulcrum.EmbeddedBrowser.Browser browser, string functionname, ZenFulcrum.EmbeddedBrowser.JSONNode[] param,
                System.Action<ZenFulcrum.EmbeddedBrowser.JSONNode> result = null)
            {
                if(browser)
                {
                    var retVal = param != null && param.Length > 0 ? browser.CallFunction(functionname, param) : browser.CallFunction(functionname);
                    if(retVal != null)
                    {
                        retVal.Done((_ret) =>
                        {
                            if(result != null)
                            {
                                result.Invoke(_ret);
                            }
                        });
                    }
                }
            }
            #endregion
    
    
            #region 代码注入相关
            #region JS Codes
            public const string GetInputs_JS_Name = "GetInputs";
            public const string GetInputs_JS = @"
    function GetInputs() {
        var inputs, index;
        var array = new Array();
        inputs = document.getElementsByTagName('input');
        for (index = 0; index < inputs.length; ++index) {
            array.push(inputs[index].id);
        }
        return array;
    }";
    
            public const string InjectInputID_JS_Name = "InjectInputID";
            public const string InjectInputID_JS = @"
    var inputID = 1;
    function InjectInputID() {
        var inputs, index;
        inputs = document.getElementsByTagName('input');
        for (index = 0; index < inputs.length; ++index) {
            var rawID = inputs[index].id;
            if(rawID == null || rawID == ''){
                inputs[index].id = 'custom_input_id_' + (inputID++);
            }
        }
    }";
    
            public const string SetInputValue_JS_Name = "SetInputValue";
            public const string SetInputValue_JS = @"
    function SetInputValue(id, str) {
        var target = document.getElementById(id);
        if (target != null) { target.value = str; }
    }";
    
            public const string HasFocusFunc_JS_Name = "GetHasFocus";
            public const string HasFocusFunc_JS = @"
    function GetHasFocus(id) {
        var target = document.getElementById(id);
        return (target != null && target.id == document.activeElement.id);
    }";
    
            public const string CancelFocus_JS_Name = "CancelFocus";
            public const string CancelFocus_JS = @"
    function CancelFocus() {
        document.activeElement.blur();
    }";
    
            public const string AddEventFunc_JS_Name = @"AddEventFunc";
            public const string eventFuncNameTemplate_JS = "EVENTFUNC";
            public const string eventTemplateName_JS = "EVENTNAME";
            public const string AddEventFuncTemplate_JS = @"
    function AddEventFunc(elementID) 
    {
        var tagElement = document.getElementById(elementID);
        if (tagElement != null) 
        {
            var oldFuncStr = (tagElement.EVENTFUNC + '').replace(/(
    )+|(
    )+/g, '');
            var rawFunc = oldFuncStr.substring(oldFuncStr.indexOf('{') + 1, oldFuncStr.indexOf('}'));
            var newFunc = function() 
            {
                eval(rawFunc);
                console.log(elementID + ':EVENTNAME');
            }
            if((newFunc + '').replace(/(
    )+|(
    )+/g, '') == oldFuncStr){
                return;
            }
            tagElement.EVENTFUNC = newFunc;
        }
    }";
    
            const string GetElementRect_JS_Name = "GetElementRect";
            const string GetElementRect_JS = @"
    function GetElementRect(id)
    {
        var tag = document.getElementById(id);
        var rect = tag.getBoundingClientRect();
        var region = {};
        region['x'] = rect.left;
        region['y'] = rect.top;
        region['width'] = Math.abs(rect.right - rect.left);
        region['height'] = Math.abs(rect.top - rect.bottom);
        return region;
    }";
    
            #endregion
    
            public enum ElementEventFunc
            {
                onclick,
                onsubmit,
                onfocus
            }
            public static string GenerateAddEventFunc_JS_Code(ElementEventFunc eventFunc, string customEvent)
            {
                return GenerateAddEventFunc_JS_Code(eventFunc.ToString(), customEvent);
            }
            public static string GenerateAddEventFunc_JS_Code(string eventFunc, string customEvent)
            {
                return AddEventFuncTemplate_JS.Replace(eventFuncNameTemplate_JS, eventFunc).Replace(eventTemplateName_JS, customEvent);
            }
            #endregion
        }
    }
    AutoBrowserInputField
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    
    namespace UIModules
    {
        public class BrowserInputFieldPanel : UIBase
        {
            [SerializeField]
            public UIModules.UITools.BrowserInputField browserInputFieldTemplate;
        }
    }
    BrowserInputFieldPanel

      

      

  • 相关阅读:
    Pythoy 数据类型序列化——json&pickle 模块
    Python xml 模块
    Spring MVC <mvc:annotation-driven/>的作用
    Spring MVC学习笔记
    springboot配置logback日志
    Git master合并分支时提示“Already up-to-date”
    解决idea tomcat乱码问题
    MYSQL 八大优化方案
    SpringBoot项目集成PageHelper使用
    Git--远程仓库版本回退方法
  • 原文地址:https://www.cnblogs.com/tiancaiwrk/p/13231186.html
Copyright © 2011-2022 走看看