网页嵌入插件最好的应该就是ZFBrowser了, 可是使用起来也是问题多多, 现在最要命的是网页输入不能打中文, 作者也没打算接入IME, 只能自己想办法了...
搞了半天只想到一个办法, 就是通过Unity的IME去触发中文输入, 然后传入网页, 也就是说做一个透明的 InputField 盖住网页的输入文本框, 然后在 Update 或是 onValueChanged 中把内容传给网页, 这样基本就能实现中文输入了.
因为对前端不熟悉, 我就做了一个简单网页做测试:
<html> <head> <title>My first pagetitle> <style> body { margin: 0; } style> head> <body> <h1>Test Inputh1> 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().alpha = 0.01f; Init(); } public void Init() { if(input && (false == inited)) { inited = true; input.RequireComponent (); // IME 跟随 StartCoroutine(CaretAccess((_caret) => { if(_caret) { var group = _caret.RequireComponent (); group.alpha = 1f; group.ignoreParentGroups = true; } })); } } IEnumerator CaretAccess(System.Action 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: 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 IDbutton> <br> <p id="show">Showp> <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.Actionsucc = 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 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 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取消的时候销毁它, 就能完美解决输入法问题了...