导入对应的3D WebView资产
创建好对应的Prefab结构
自定义输入检测
WebView默认的输入检测系统监听于Eventsystem,而NGUI的输入事件是由UICamera控制的,所以我们需要自定义WebView的输入检测,让它监听于UIEventListener才能够正确处理输入事件。WebView提供了IPointerInputDetector接口用于自定义输入检测,我们只需实现这个接口,然后将其传递给WebViewPrefab.SetPointerInputDetector()。需要特别注意的是,WebView接收的Vector2参数为标准化点,这里标准化点指的是x和y坐标在[0,1]范围内的Vector2,[0,0]为左上角,[1,1]为右下角,y轴正方向为下,x轴正方向为右。
```c#
using System;
using UnityEngine;
namespace Vuplex.WebView {
public class NGUIPointerInputDetector : MonoBehaviour, IPointerInputDetector {
public bool PointerMovedEnabled { get; set; }
public event EventHandler<EventArgs<Vector2>> BeganDrag;
public event EventHandler<EventArgs<Vector2>> Dragged;
public event EventHandler<PointerEventArgs> PointerDown;
public event EventHandler PointerEntered;
public event EventHandler<EventArgs<Vector2>> PointerExited;
public event EventHandler<EventArgs<Vector2>> PointerMoved;
public event EventHandler<PointerEventArgs> PointerUp;
public event EventHandler<ScrolledEventArgs> Scrolled;
private UIWidget m_Widget;
void Start()
{
if (!this.TryGetComponent<UIWidget>(out m_Widget))
{
Debug.LogError(string.Format("{0} 没有UIWidget组件", this.gameObject.name));
}
var _UIEventListener = UIEventListener.Get(this.gameObject);
_UIEventListener.onDragStart += (GameObject go) =>
{
var eventArgs = new EventArgs<Vector2>(_convertToNormalizedPoint(Input.mousePosition));
_raiseDraggedEvent(eventArgs);
};
_UIEventListener.onDrag += (GameObject go, Vector2 delta) =>
{
var eventArgs = new EventArgs<Vector2>(_convertToNormalizedPoint(Input.mousePosition));
_raiseDraggedEvent(eventArgs);
};
_UIEventListener.onPress += (GameObject go, bool state) =>
{
PointerEventArgs eventArgs = new PointerEventArgs();
eventArgs.ClickCount = 1;
eventArgs.Point = _convertToNormalizedPoint(UICamera.currentTouch.pos);
if (state)
{
_raisePointerDownEvent(eventArgs);
}
else
{
_raisePointerUpEvent(eventArgs);
}
};
_UIEventListener.onHover += (GameObject go, bool state) =>
{
if (state)
{
_raisePointerEnteredEvent(EventArgs.Empty);
}
else
{
var eventArgs = new EventArgs<Vector2>(_convertToNormalizedPoint(Input.mousePosition));
_raisePointerExitedEvent(eventArgs);
}
_isHovering = state;
};
_UIEventListener.onScroll += (GameObject go, float delta) =>
{
var scrollDelta = -1 * new Vector2(0, delta * m_Widget.height);
_raiseScrolledEvent(new ScrolledEventArgs(scrollDelta, _convertToNormalizedPoint(Input.mousePosition)));
};
}
protected virtual Vector2 _convertToNormalizedPoint(Vector2 uiPos)
{
// 当前widget屏幕坐标原点
Vector2 zeroPoint = new Vector2(-m_Widget.width / 2, m_Widget.height / 2);
// 转换为世界坐标
Vector3 worldPosition = UICamera.mainCamera.ScreenToWorldPoint(new Vector3(pos.x, pos.y, 0));
// 转换到本地坐标
Vector3 localPosition = transform.InverseTransformPoint(worldPosition);
// 基于原点坐标的增量
Vector2 posBasedZero = new Vector2(localPosition.x - zeroPoint.x, -(localPosition.y - zeroPoint.y));
// 归一化
Vector2 normalizedPoint = new Vector2();
normalizedPoint.x = Mathf.Clamp01(posBasedZero.x / m_Widget.width);
normalizedPoint.y = Mathf.Clamp01(posBasedZero.y / m_Widget.height);
return normalizedPoint;
}
protected void _raiseDraggedEvent(EventArgs<Vector2> eventArgs) => Dragged?.Invoke(this, eventArgs);
protected void _raisePointerDownEvent(PointerEventArgs eventArgs) => PointerDown?.Invoke(this, eventArgs);
protected void _raisePointerUpEvent(PointerEventArgs eventArgs) => PointerUp?.Invoke(this, eventArgs);
bool _isHovering;
protected void _raisePointerEnteredEvent(EventArgs eventArgs) => PointerEntered?.Invoke(this, eventArgs);
protected void _raisePointerExitedEvent(EventArgs<Vector2> eventArgs) => PointerExited?.Invoke(this, eventArgs);
protected void _raisePointerMovedEvent(EventArgs<Vector2> eventArgs) => PointerMoved?.Invoke(this, eventArgs);
void _raisePointerMovedIfNeeded()
{
if (!(PointerMovedEnabled && _isHovering))
{
return;
}
var point = _convertToNormalizedPoint(Input.mousePosition);
if (!(point.x >= 0f && point.y >= 0f))
{
// This can happen while the prefab is being resized.
return;
}
_raisePointerMovedEvent(new EventArgs<Vector2>(point));
}
protected void _raiseScrolledEvent(ScrolledEventArgs eventArgs) => Scrolled?.Invoke(this, eventArgs);
private void Update()
{
_raisePointerMovedIfNeeded();
}
}
}
```
重写ViewportMaterialView.cs
WebView演示demo中承载webView渲染内容的是MeshRender和UGUI的RawImg,NGUI的话我们替换为UITexture,要做的很简单,替换代码中相应的类即可。
```c#
using UnityEngine;
using Vuplex.WebView.Internal;
namespace Vuplex.WebView {
public class NGUIViewportMaterialView : ViewportMaterialView {
// Start is called before the first frame update
public override Material Material {
get {
return GetComponent<UITexture>().material;
}
set {
GetComponent<UITexture>().material = value;
if (value.mainTexture != null)
{
GetComponent<UITexture>().mainTexture = value.mainTexture;
}
}
}
public override Texture Texture {
get {
return GetComponent<UITexture>().material.mainTexture;
}
set {
GetComponent<UITexture>().material.mainTexture = value;
GetComponent<UITexture>().mainTexture = value;
}
}
public override bool Visible {
get {
return GetComponent<UITexture>().enabled;
}
set {
GetComponent<UITexture>().enabled = value;
}
}
public override void SetCropRect(Rect rect)
{
GetComponent<UITexture>().material.SetVector("_CropRect", _rectToVector(rect));
}
public override void SetCutoutRect(Rect rect)
{
var rectVector = _rectToVector(rect);
if (rect != new Rect(0, 0, 1, 1))
{
// Make the actual cutout slightly smaller (2% shorter and 2% skinnier) so that
// the gap between the video layer and the viewport isn't visible.
// This is only done if the rect doesn't cover the entire view, because
// the Keyboard component uses a rect cutout of the entire view for Android Gecko.
var onePercentOfWidth = rect.width * 0.01f;
var onePercentOfHeight = rect.height * 0.01f;
rectVector = new Vector4(
rectVector.x + onePercentOfWidth,
rectVector.y + onePercentOfHeight,
rectVector.z - 2 * onePercentOfWidth,
rectVector.w - 2 * onePercentOfHeight
);
}
GetComponent<UITexture>().material.SetVector("_VideoCutoutRect", rectVector);
}
Vector4 _rectToVector(Rect rect) => new Vector4(rect.x, rect.y, rect.width, rect.height);
}
}
```
重写BaseWebViewPrefab.cs
这里主要是重写了_getRectForInitialization()方法,其他代码可以参考CanvasWebViewPrefab.cs,然后添加了UrlChangedCallBack,InitializedCallBack两个回调事件供lua端使用。
```c#
using System;
using UnityEngine;
using UnityEngine.Serialization;
using Vuplex.WebView.Internal;
namespace Vuplex.WebView {
public partial class NGUIWebViewPrefab : BaseWebViewPrefab {
public System.Action<string> UrlChangedCallBack;
public System.Action InitializedCallBack;
[Label("Native On-Screen Keyboard (Android and iOS only)")]
[Tooltip("Determines whether the operating system's native on-screen keyboard is automatically shown when a text input in the webview is focused. The native on-screen keyboard is only supported for the following packages:\n• 3D WebView for Android (non-Gecko)\n• 3D WebView for iOS")]
public bool NativeOnScreenKeyboardEnabled = true;
[Label("Resolution (px / Unity unit)")]
[Tooltip("You can change this to make web content appear larger or smaller. Note that This property is ignored when running in Native 2D Mode.")]
[HideInInspector]
[FormerlySerializedAs("InitialResolution")]
public float Resolution = 1;
[HideInInspector]
[Tooltip("Determines the scroll sensitivity. Note that This property is ignored when running in Native 2D Mode.")]
public float ScrollingSensitivity = 15;
protected override bool _getNativeOnScreenKeyboardEnabled()
{
return NativeOnScreenKeyboardEnabled;
}
protected override float _getResolution()
{
if (Resolution > 0f)
{
return Resolution;
}
WebViewLogger.LogError("Invalid value set for NGUIWebViewPrefab.Resolution: " + Resolution);
return 1;
}
protected override float _getScrollingSensitivity() => ScrollingSensitivity;
protected override ViewportMaterialView _getVideoLayer()
{
var obj = transform.Find("VideoLayer");
return obj == null ? null : obj.GetComponent<ViewportMaterialView>();
}
protected override ViewportMaterialView _getView()
{
var obj = transform.Find("NGUIWebViewPrefabView");
return obj == null ? null : obj.GetComponent<ViewportMaterialView>();
}
protected override void _setVideoLayerPosition(Rect videoRect)
{
var _viodeLayerUIWidget = _videoLayer.gameObject.GetComponent<UIWidget>();
_viodeLayerUIWidget.SetRect(videoRect.x, videoRect.y, videoRect.width, videoRect.height);
}
async void _initNGUIPrefab()
{
try
{
OnInit();
var rect = _getRectForInitialization();
if (_logErrorIfSizeIsInvalid(rect.size))
{
return;
}
await _initBase(rect);
this.WebView.UrlChanged += (sender, eventArgs) => {
this.UrlChangedCallBack?.Invoke(eventArgs.Url);
};
}
catch (Exception exception)
{
// Catch any exceptions that occur during initialization because
// some applications terminate the application on uncaught exceptions.
Debug.LogException(exception);
}
}
public void AddUrlChangedCallBack(System.Action<string> urlChangedCallBack)
{
this.UrlChangedCallBack += urlChangedCallBack;
}
public void RemoveUrlChangedCallBack(System.Action<string> urlChangedCallBack)
{
this.UrlChangedCallBack -= urlChangedCallBack;
}
public void AddInitializedCallBack(System.Action initializedCallBack)
{
this.InitializedCallBack += initializedCallBack;
}
public void RemoveInitializedCallBack(System.Action initializedCallBack)
{
this.InitializedCallBack -= initializedCallBack;
}
Rect _getRectForInitialization()
{
Vector4 border = _uiWidget.border; // 获取边框
Vector2 dimensions = new Vector2(_uiWidget.width, _uiWidget.height); // 获取尺寸
// 计算矩形
float xMin = _uiWidget.transform.position.x - (dimensions.x / 2) + border.x; // 左边界
float xMax = _uiWidget.transform.position.x + (dimensions.x / 2) - border.z; // 右边界
float yMin = _uiWidget.transform.position.y - (dimensions.y / 2) + border.y; // 下边界
float yMax = _uiWidget.transform.position.y + (dimensions.y / 2) - border.w; // 上边界
return new Rect(xMin, yMin, xMax - xMin, yMax - yMin);
}
private void OnInit()
{
this.Initialized += (sender, eventArgs) =>
{
this.InitializedCallBack?.Invoke();
};
}
bool _sizeIsInvalid(Vector2 size) => !(size.x > 0f && size.y > 0f);
bool _logErrorIfSizeIsInvalid(Vector2 size)
{
if (_sizeIsInvalid(size))
{
WebViewLogger.LogError($"NGUIWebViewPrefab dimensions are invalid! Width: {size.x.ToString("f4")}, Height: {size.y.ToString("f4")}. ");
return true;
}
return false;
}
#region Non-public members
UIWidget _cachedUIWidget;
UIWidget _uiWidget {
get {
if (_cachedUIWidget == null)
{
_cachedUIWidget = GetComponent<UIWidget>();
}
return _cachedUIWidget;
}
}
#endregion
#region monoBehaviour events
private void Start()
{
_initNGUIPrefab();
}
protected override void Update()
{
base.Update();
if (WebView == null)
{
return;
}
_sizeInUnityUnits = new Vector2(_uiWidget.width, _uiWidget.height);
if (_logErrorIfSizeIsInvalid(_sizeInUnityUnits))
{
return;
}
// Handle resizing a regular webview.
_resizeWebViewIfNeeded();
}
#endregion
}
}
```
点击链接时跳转外部浏览器和提供接口给lua端使用
由于lua端无法处理异步方法,所以初始化WebView放在了c#端,所以在lua端在使用WebView时需等到InitializedCallBack回调之后。
然后就是给WebView.PageLoadScripts添加了一段JavaScript供点击链接时向客户端发送链接信息,并阻止WebView加载URL,WebView.MessageEmitted接收到信息并作出相应处理,这里选择使用UrlLoadedCallBack事件将逻辑放到lua端处理。
```c#
using UnityEngine;
namespace Vuplex.WebView {
public class NGUIWebViewPortToLua : MonoBehaviour {
private BaseWebViewPrefab baseWebViewPrefab;
public System.Action<string> UrlLoadedCallBack;
private async void Start()
{
baseWebViewPrefab = GetComponent<BaseWebViewPrefab>();
await baseWebViewPrefab.WaitUntilInitialized();
// Add JavaScript to PageLoadScripts.
// https://developer.vuplex.com/webview/IWebView#PageLoadScripts
baseWebViewPrefab.WebView.PageLoadScripts.Add(@"
// Use setInterval() to run this code every 250 ms.
setInterval(() => {
// Detect <a> tags that don't have our custom 'overridden' attribute
// and update them to add the 'overridden' attribute and send the link
// URL to the app's C# via window.vuplex.postMessage() when the link is clicked.
const newLinks = document.querySelectorAll('a[href]:not([overridden])');
for (const link of newLinks) {
link.setAttribute('overridden', true);
const linkUrl = link.href;
link.addEventListener('click', event => {
// Send the link URL to the C# script.
window.vuplex.postMessage('link:' + linkUrl);
// Call preventDefault() to prevent the webview from loading the URL.
event.preventDefault();
});
}
}, 250);
");
// Listen for messages from JavaScript.
// https://support.vuplex.com/articles/how-to-send-messages-from-javascript-to-c-sharp
baseWebViewPrefab.WebView.MessageEmitted += (sender, eventArgs) =>
{
var message = eventArgs.Value;
var prefix = "link:";
if (message.StartsWith(prefix))
{
var linkUrl = message.Substring(prefix.Length);
this.UrlLoadedCallBack?.Invoke(linkUrl);
}
};
}
public void LoadUrl(string url)
{
if (baseWebViewPrefab.WebView == null)
{
Debug.Log("WebView未初始化完成");
}
baseWebViewPrefab.WebView.LoadUrl(url);
}
public void AddUrlBeforeLoadCallBack(System.Action<string> urlBeforeLoadCallBack)
{
this.UrlLoadedCallBack += urlBeforeLoadCallBack;
}
public void RemoveUrlBeforeLoadCallBack(System.Action<string> urlBeforeLoadCallBack)
{
this.UrlLoadedCallBack -= urlBeforeLoadCallBack;
}
}
}
```
lua端启动
```lua
---@class WebWidget : LuaBehaviour
local WebWidget = newClass("WebWidget", LuaBehaviour)
-- 初始化 这里是初始化webView界面时调用,读者可以根据自身项目逻辑处理
function WebWidget:InitWidget(...)
self:ConstructMember()
self:InitMember()
end
function WebWidget:ConstructMember()
--region SerializeField注入
---@field NGUIWebViewPrefab GameObject
self.NGUIWebViewPrefab = nil
UIStorage.FillTableAttr(self)
--endregion
-- 是否初始化WebView
self.m_isInitWebView = false
self.m_nguiWebViewPrefab = nil
self.m_nguiWebViewPortToLua = nil
self.m_url = ""
end
function WebWidget:InitMember()
self.m_nguiWebViewPrefab = self.NGUIWebViewPrefab:GetComponent("NGUIWebViewPrefab")
self.m_nguiWebViewPortToLua = self.NGUIWebViewPrefab:GetComponent("NGUIWebViewPortToLua")
self.m_nguiWebViewPrefab:AddUrlChangedCallBack(function(url)
print("UrlChanged: " .. url)
end)
self.m_url = "https://www.unity.com"
self.m_nguiWebViewPrefab.InitialUrl = self.m_url
self.m_nguiWebViewPrefab:AddInitializedCallBack(function()
self.m_isInitWebView = true
end)
self.m_nguiWebViewPortToLua:AddUrlLoadedCallBack(function(linkUrl)
Application.OpenURL(linkUrl)
end)
end
-- 每次打开webView界面时调用
function WebWidget:Show()
if self.m_isInitWebView then
self.m_nguiWebViewPortToLua:LoadUrl(self.m_url)
end
end
-- 关闭
function WebWidget:Close()
end
```