3DWebView适配NGUI

327 阅读5分钟

导入对应的3D WebView资产

官网链接

创建好对应的Prefab结构

NGUIWebViewPrefab结构.png NGUIWebViewPrefabInspector.png

自定义输入检测

WebView默认的输入检测系统监听于Eventsystem,而NGUI的输入事件是由UICamera控制的,所以我们需要自定义WebView的输入检测,让它监听于UIEventListener才能够正确处理输入事件。WebView提供了IPointerInputDetector接口用于自定义输入检测,我们只需实现这个接口,然后将其传递给WebViewPrefab.SetPointerInputDetector()。需要特别注意的是,WebView接收的Vector2参数为标准化点,这里标准化点指的是xy坐标在[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,然后添加了UrlChangedCallBackInitializedCallBack两个回调事件供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
```