版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/xiaoyang0611/article/details/49128077
egret的WebView实现(基于egret2.5)
标签(空格分隔): egret webview
客户端开发中有一种很常见的场景就是展现web页面,也就是 WebView 。例如 IOS 、 Android ,都为开发者提供了 WebView 组件,而在 egret 中暂时并没有提供 WebView 组件,所以我们只能自己动手实现一个能在 egret 引擎中可用的 WebView 组件。
由于egret运行在浏览器中,所以想要实现一个能展现web页面的组件,第一想法应该是使用iframe。
那么这些问题需要解决:
与 egret 集成,并提供与 egret 组件一致的调用方式
融入 egret 的展现,即 WebView 的坐标、大小需要与 egret 一致
查看 egret 源码,我们不难发现 egret 的 HtmlInput 组件也是使用上面我们所考虑的方式实现在游戏中进行文本输入的。所以,我们可以采用 egret HtmlInput 类似的方式实现 WebView 。因此,我们首先从 egret HtmlInuput 源码入手:
- Egret中HtmlInput的实现
1.1 HtmlInput实现方式:
在 egret.web.js 中可以很找到 egret.web.HTMLInput 的初始化相关的源码:
HtmlInput.prototype._initStageDelegateDiv = function (container, canvas) {
this.canvas = canvas;
var self = this;
var stageDelegateDiv;
if (!stageDelegateDiv) {
stageDelegateDiv = document.createElement("div");
this.StageDelegateDiv = stageDelegateDiv;
stageDelegateDiv.id = "StageDelegateDiv";
container.appendChild(stageDelegateDiv);
self.initValue(stageDelegateDiv);
self._inputDIV = document.createElement("div");
self.initValue(self._inputDIV);
self._inputDIV.style.width = "0px";
self._inputDIV.style.height = "0px";
self._inputDIV.style.left = 0 + "px";
self._inputDIV.style.top = "-100px";
self._inputDIV.style[egret.web.getPrefixStyleName("transformOrigin")] = "0% 0% 0px";
stageDelegateDiv.appendChild(self._inputDIV);
this.canvas.addEventListener("click", function (e) {
if (self._needShow) {
self._needShow = false;
self._stageText._onClickHandler(e);
self.show();
}
else {
if (self._inputElement) {
self.clearInputElement();
self._inputElement.blur();
self._inputElement = null;
}
}
});
self.initInputElement(true);
self.initInputElement(false);
}
};
HtmlInput.prototype.initInputElement = function (multiline) {
var self = this;
//增加1个空的textarea
var inputElement;
if (multiline) {
inputElement = document.createElement("textarea");
inputElement.style["resize"] = "none";
self._multiElement = inputElement;
inputElement.id = "egretTextarea";
}
else {
inputElement = document.createElement("input");
self._simpleElement = inputElement;
inputElement.id = "egretInput";
}
inputElement.type = "text";
self._inputDIV.appendChild(inputElement);
inputElement.setAttribute("tabindex", "-1");
inputElement.style.width = "1px";
inputElement.style.height = "12px";
self.initValue(inputElement);
inputElement.style.outline = "thin";
inputElement.style.background = "none";
inputElement.style.overflow = "hidden";
inputElement.style.wordBreak = "break-all";
//隐藏输入框
inputElement.style.opacity = 0;
inputElement.oninput = function () {
if (self._stageText) {
self._stageText._onInput();
}
};
};
很明显,是在id为 StageDelegateDiv 的 dom 中嵌入 和 ,并在这两个 html 元素上做文章。
1.2 HtmlInput 渲染展现:
那么 text 输入框和 textarea 文本输入域是如何正确展现到 egret 的 canvas 上的呢?
在 egret.web.js 的 egret.web.HTML5StageText 中我们看到如下代码:
p.$setTextField = function (textfield) {
this.$textfield = textfield;
return true;
};
/**
* @private
*
*/
p.$addToStage = function () {
this.htmlInput = egret.web.$getTextAdapter(this.$textfield);
};
/**
* @private
*
*/
p._initElement = function () {
var point = this.$textfield.localToGlobal(0, 0);
var x = point.x;
var y = point.y;
var cX = this.$textfield.$renderMatrix.a;
var cY = this.$textfield.$renderMatrix.d;
var scaleX = this.htmlInput.$scaleX;
var scaleY = this.htmlInput.$scaleY;
this.inputDiv.style.left = x * scaleX + "px";
this.inputDiv.style.top = y * scaleY + "px";
if (this.$textfield.multiline) {
this.inputDiv.style.top = (y) * scaleY + "px";
this.inputElement.style.top = (-this.$textfield.lineSpacing / 2) + "px";
}
else {
this.inputDiv.style.top = y * scaleY + "px";
this.inputElement.style.top = 0 + "px";
}
this._gscaleX = scaleX * cX;
this._gscaleY = scaleY * cY;
};
从这几段关键代码中我们可以看到, HtmlInput 的文本使用 egret.TextFiled 组件进行渲染的,并且 HtmlInput 对应的 Dom 位置、宽高直接使用 TextFiled 的位置、宽高,这样 HtmlInputDom 就能与 egret 的坐标宽高一致了。
1.3 HtmlInput文本输入:
那么是怎么实现输入的呢?
在 egret.web.js 的 egret.web.HTML5StageText 的源码中能找到如下代码:
HTML5StageText.prototype.executeShow = function () {
var self = this;
//打开
this.inputElement.value = this.
g
e
t
T
e
x
t
(
)
;
i
f
(
t
h
i
s
.
i
n
p
u
t
E
l
e
m
e
n
t
.
o
n
b
l
u
r
=
=
n
u
l
l
)
t
h
i
s
.
i
n
p
u
t
E
l
e
m
e
n
t
.
o
n
b
l
u
r
=
t
h
i
s
.
o
n
B
l
u
r
H
a
n
d
l
e
r
.
b
i
n
d
(
t
h
i
s
)
;
t
h
i
s
.
getText(); if (this.inputElement.onblur == null) { this.inputElement.onblur = this.onBlurHandler.bind(this); } this.
getText();if(this.inputElement.onblur==null)this.inputElement.onblur=this.onBlurHandler.bind(this);this.resetStageText();
if (this.KaTeX parse error: Expected '}', got 'EOF' at end of input: …xlength", this.textfield.maxChars);
}
else {
this.inputElement.removeAttribute(“maxlength”);
}
this.inputElement.selectionStart = this.inputElement.value.length;
this.inputElement.selectionEnd = this.inputElement.value.length;
this.inputElement.focus();
};
从最后一行代码可以看到,直接调用 HtmlInput 对应的 dom 的 onfocus() ,从而弹出输入框,即可进行文本输入操作。
- WebView的实现
看完 HtmlInput 的实现,可见 WebView 的实现有所不同:
HtmlInput 依赖 egret.TextField 来进行渲染展现,egret.TextField 是 egret 中内建的组件,所以 HtmlInput 坐标宽高直接使用 textfield 的坐标、宽高就可以了。但是 WebView 依赖的是 iframe ,是 html 的标准组件,并不是 egret 的内建组件,而且在 egret 的 canvas 之外,所以必然会涉及到 webview 组件的坐标宽高的换算。结合上文提到的 WebView 应该达到的效果,我们可以考虑如下实 现WebView :
WebView 继承degret.displayObjectContailner,override相关方法(getX,getY,setX,setY,getWidht,getHeight,setWidth,setHeight等)以提供与egret引擎中内建组件一致的调用方式:
class WebView extends egret.DisplayObjectContainer {
private _x:number=0;
private _y:number=0;
private _width:number=0;
private _height:number=0;
private _src:string="";
//getter setter ...
}
与 HtmlInput 一样,在 StageDelegatDiv 中 加入 iframe, WebView 对象持有此 iframe dom 的引用,但是必须根据其 x,y,width,height 进行相应的坐标、大小的转化并进行显示:
要实现以上想法,以下需要处理:
(1) 了解egret的scalemode( 缩放模式)
(2) 根据各种scalemode的实现方式换算webview的x,y,width,height
(3) 控制WebView中iframe的样式及显示时机
2.1 egret 的 scalemode (缩放模式)
egret的六种 scalemode :
StageScaleMode.NO_SCALE = "noScale";
StageScaleMode.SHOW_ALL = "showAll";
StageScaleMode.NO_BORDER = "noBorder";
StageScaleMode.EXACT_FIT = "exactFit";
StageScaleMode.FIXED_WIDTH = "fixedWidth";
StageScaleMode.FIXED_HEIGHT = "fixedHeight";
StageScaleMode.NO_SCALE:
不缩放应用程序内容。即使在更改播放器视口大小时,它仍然保持不变。如果播放器视口比内容小,则可能进行一些裁切。 在此模式下,舞台尺寸(Stage.stageWidth,Stage.stageHeight)始终跟播放器视口大小保持一致。
StageScaleMode.SHOW_ALL:
保持原始宽高比缩放应用程序内容,缩放后应用程序内容的较宽方向填满播放器视口,另一个方向的两侧可能会不够宽而留有黑边。在此模式下,舞台尺寸(Stage.stageWidth,Stage.stageHeight)始终等于初始化时外部传入的应用程序内容尺寸。
StageScaleMode.NO_BORDER:
保持原始宽高比缩放应用程序内容,缩放后应用程序内容的较窄方向填满播放器视口,另一个方向的两侧可能会超出播放器视口而被裁切在此模式下,舞台尺寸(Stage.stageWidth,Stage.stageHeight)始终等于初始化时外部传入的应用程序内容尺寸。
StageScaleMode.EXACT_FIT:
不保持原始宽高比缩放应用程序内容,缩放后应用程序内容正好填满播放器视口。在此模式下,舞台尺寸(Stage.stageWidth,Stage.stageHeight)始终等于初始化时外部传入的应用程序内容尺寸。
StageScaleMode.FIXED_WIDTH :
保持原始宽高比缩放应用程序内容,缩放后应用程序内容在水平和垂直方向都填满播放器视口,但只保持应用程序内容的原始高度不变,宽度可能会改变。在此模式下,舞台高度(Stage.stageHeight)始终等于初始化时外部传入的应用程序内容高度。舞台宽度(Stage.stageWidth)由当前的缩放比例与播放器视口宽度决定。
StageScaleMode.FIXED_HEIGHT :
保持原始宽高比缩放应用程序内容,缩放后应用程序内容在水平和垂直方向都填满播放器视口,但只保持应用程序内容的原始高度不变,宽度可能会改变。在此模式下,舞台高度(Stage.stageHeight)始终等于初始化时外部传入的应用程序内容高度。舞台宽度(Stage.stageWidth)由当前的缩放比例与播放器视口宽度决定。
引用官方的一张图:
egret官方文章的介绍: egret scalemode ,有很详细的讲解。
可见,不同的缩放模式对于舞台大小、显示宽高的大小有决定性的影响。我们仅考虑SHOW_ALL,NO_BORDER,FIXED_WIDTH,FIXED_HEIGHT这几种在实际生产中用得比较多的缩放模式。
各种缩放模式下是如何计算舞台宽高、显示宽高的呢?
在egret.js的源码中,我们可以找到如下代码片段:
/**
* @private
* 计算舞台显示尺寸
* @param scaleMode 当前的缩放模式
* @param screenWidth 播放器视口宽度
* @param screenHeight 播放器视口高度
* @param contentWidth 初始化内容宽度
* @param contentHeight 初始化内容高度
*/
ScreenAdapter.prototype.calculateStageSize = function (scaleMode, screenWidth, screenHeight, contentWidth, contentHeight) {
var displayWidth = screenWidth;
var displayHeight = screenHeight;
var stageWidth = contentWidth;
var stageHeight = contentHeight;
var scaleX = (screenWidth / stageWidth) || 0;
var scaleY = (screenHeight / stageHeight) || 0;
switch (scaleMode) {
case egret.StageScaleMode.EXACT_FIT:
break;
case egret.StageScaleMode.FIXED_HEIGHT:
stageWidth = Math.round(screenWidth / scaleY);
break;
case egret.StageScaleMode.FIXED_WIDTH:
stageHeight = Math.round(screenHeight / scaleX);
break;
case egret.StageScaleMode.NO_BORDER:
if (scaleX > scaleY) {
displayHeight = Math.round(stageHeight * scaleX);
}
else {
displayWidth = Math.round(stageWidth * scaleY);
}
break;
case egret.StageScaleMode.SHOW_ALL:
if (scaleX > scaleY) {
displayWidth = Math.round(stageWidth * scaleY);
}
else {
displayHeight = Math.round(stageHeight * scaleX);
}
break;
default:
stageWidth = screenWidth;
stageHeight = screenHeight;
break;
}
return {
stageWidth: stageWidth,
stageHeight: stageHeight,
displayWidth: displayWidth,
displayHeight: displayHeight
};
2.2 根据各种 scalemode 的实现方式换算 webview 的 x,y,width,height
我们可以清楚看到各种缩放模式下,舞台宽高、显示宽高的计算,因此我们可以根据相应的缩放模式来换算WebView的x,y,widht,height对应在web窗口中的具体数值,这里以WebView.setwidth(w:number)为例:
public set width(value:number) {
this._width = value;
if(this._scaleMode==egret.StageScaleMode.FIXED_WIDTH || this._scaleMode==egret.StageScaleMode.FIXED_HEIGHT ){
this._iframe.width=this._width/this._stageW*this._windowW+"px";
this._iframeWrapper.style.width=this._width/this._stageW*this._windowW+"px";
}
if(this._scaleMode==egret.StageScaleMode.SHOW_ALL || this._scaleMode==egret.StageScaleMode.NO_BORDER ) {
if(this._windowW==this._displayW){
this._iframe.style.width = this._width / this._stageW * this._windowW + "px";
this._iframeWrapper.style.width = this._width / this._stageW * this._windowW + "px";
}else{
this._iframe.style.width = this._width / this._stageW * this._displayW + "px";
this._iframeWrapper.style.width = this._width / this._stageW * this._displayW + "px";
}
}
}
setX,setY,setHeight等方法坐标换算与上述方式相同。
通过坐标换算,我们就能以egret组件一样的方式调用WebView并且展现在正确的位置。
2.3 控制 WebView 中 iframe 的样式及显示时机
坐标宽高都确定了,就可以设置 iframe 的样式了,这个比较简单,通过js即可设置样式。另外,iframe在网页完全加载完再显示会比较友好。
- IOS 系统中 iframe 宽高撑大、无法滑动的解决方案
测试如上方法开发的 WebView 时,发现在 ios 中, iframe 宽高会被里面的内容撑大、并且无法滑动:
通过google,找到了解决方案:
http://stackoverflow.com/questions/23083462/how-to-get-an-iframe-to-be-responsive-in-ios-safari
或者
http://davidwalsh.name/scroll-iframes-ios
即:
给iframe一个父级div,设置如下css属性:
#iframe-wrapper {
-webkit-overflow-scrolling: touch;
overflow-y: scroll;
/* important: dimensions or positioning here! */
}
#iframe-wrapper iframe {
/* nada! */
}
private _iframeWrapper:HTMLDivElement=null;
private _iframe:HTMLIFrameElement=null;
/**
* @param src
*/
public constructor(src:string){
super();
var stageDelegateDom:HTMLElement=document.getElementById("StageDelegateDiv"),playerContainer:HTMLElement=stageDelegateDom.parentElement;
var iframeWrapperDom=document.getElementById("iframe-wrapper");
if(!iframeWrapperDom){
iframeWrapperDom=document.createElement("div");
iframeWrapperDom.style.display="none";
iframeWrapperDom.attributes['style'].value+='position:absolute;-webkit-overflow-scrolling: touch;overflow-y: scroll;';//解决iframe在ios下的显示问题
iframeWrapperDom.id="iframe-wrapper";
stageDelegateDom.appendChild(iframeWrapperDom);
}
this._iframeWrapper=<HTMLDivElement>iframeWrapperDom;
this._iframeWrapper.style.display="none";
this._iframeWrapper.style.opacity="0";
var iframe = document.createElement("iframe"),t=new Date().getTime();
iframe.src=src;
iframe.id="webview-iframe-"+t;
iframe.name="webview-iframe-"+t;
iframe.style.position="absolute";
iframe.style.top="0";
iframe.style.left="0";
iframe.style.opacity="0";
iframe.style.display='none';
iframe.frameBorder='0';
iframe.border="0";
this._iframeWrapper.appendChild(iframe);
this._iframe=<HTMLIFrameElement>document.getElementById("webview-iframe-"+t);
var self=this;
this._iframe.οnlοad=function(){
self._iframeWrapper.style.opacity="1";
self._iframe.style.opacity="1";
}
}
- WebView.ts完整代码及使用方式:
WebView.ts:
/**
* WebView
* 适配FIXED_WIDTH、FIXED_HEIGHT、NO_BORDER、SHOW_ALL四种缩放模式
* 暂未考虑屏幕大小改变、屏幕旋转以及单页面多Webplay实例的情形
* Created by yxiao on 2015/9/30.
*/
class WebView extends egret.DisplayObjectContainer {
private _x:number=0;
private _y:number=0;
private _width:number=0;
private _height:number=0;
private _src:string="";
private _scaleMode:string=egret.MainContext.instance.stage.scaleMode;
private _stageW:number;
private _stageH:number;
private _windowW:number;
private _windowH:number;
private _displayH:number;
private _displayW:number;
private _designH:number;
private _designW:number;
private _iframeWrapper:HTMLDivElement=null;
private _iframe:HTMLIFrameElement=null;
/**
* @param src
*/
public constructor(src:string){
super();
var stageDelegateDom:HTMLElement=document.getElementById("StageDelegateDiv"),playerContainer:HTMLElement=stageDelegateDom.parentElement;
var iframeWrapperDom=document.getElementById("iframe-wrapper");
if(!iframeWrapperDom){
iframeWrapperDom=document.createElement("div");
iframeWrapperDom.style.display="none";
iframeWrapperDom.attributes['style'].value+='position:absolute;-webkit-overflow-scrolling: touch;overflow-y: scroll;';//解决iframe在ios下的显示问题
iframeWrapperDom.id="iframe-wrapper";
stageDelegateDom.appendChild(iframeWrapperDom);
}
this._iframeWrapper=<HTMLDivElement>iframeWrapperDom;
this._iframeWrapper.style.display="none";
this._iframeWrapper.style.opacity="0";
var iframe = document.createElement("iframe"),t=new Date().getTime();
iframe.src=src;
iframe.id="webview-iframe-"+t;
iframe.name="webview-iframe-"+t;
iframe.style.position="absolute";
iframe.style.top="0";
iframe.style.left="0";
iframe.style.opacity="0";
iframe.style.display='none';
iframe.frameBorder='0';
iframe.border="0";
this._iframeWrapper.appendChild(iframe);
this._iframe=<HTMLIFrameElement>document.getElementById("webview-iframe-"+t);
var self=this;
this._iframe.οnlοad=function(){
self._iframeWrapper.style.opacity="1";
self._iframe.style.opacity="1";
}
this._stageW=egret.MainContext.instance.stage.stageWidth;
this._stageH=egret.MainContext.instance.stage.stageHeight;
this._windowW=window.innerWidth;
this._windowH=window.innerHeight;
this._designH=parseInt(playerContainer.attributes['data-content-height'].value);
this._designW=parseInt(playerContainer.attributes['data-content-width'].value);
var stageSize = egret.sys.screenAdapter.calculateStageSize(egret.MainContext.instance.stage.scaleMode, this._windowW, this._windowH, this._designW, this._designH);
this._displayH=stageSize.displayHeight;
this._displayW=stageSize.displayWidth;
console.log("windowW:"+this._windowW);
console.log("stageW:"+this._stageW);
console.log("disPlayW:"+this._displayW);
console.log("windowH:"+this._windowH);
console.log("stageH:"+this._stageH);
console.log("displayH:"+this._displayH);
}
public show():void {
this._iframe.style.display='block';
this._iframeWrapper.style.display='block';
}
public destroy():void {
if(this._iframe){
this._iframeWrapper.style.display="none";
this._iframeWrapper.removeChild(this._iframe);
}
}
public get width():number {
return this._width;
}
public set width(value:number) {
this._width = value;
if(this._scaleMode==egret.StageScaleMode.FIXED_WIDTH || this._scaleMode==egret.StageScaleMode.FIXED_HEIGHT ){
this._iframe.width=this._width/this._stageW*this._windowW+"px";
this._iframeWrapper.style.width=this._width/this._stageW*this._windowW+"px";
}
if(this._scaleMode==egret.StageScaleMode.SHOW_ALL || this._scaleMode==egret.StageScaleMode.NO_BORDER ) {
if(this._windowW==this._displayW){
this._iframe.style.width = this._width / this._stageW * this._windowW + "px";
this._iframeWrapper.style.width = this._width / this._stageW * this._windowW + "px";
}else{
this._iframe.style.width = this._width / this._stageW * this._displayW + "px";
this._iframeWrapper.style.width = this._width / this._stageW * this._displayW + "px";
}
}
}
public get height():number {
return this._height;
}
public set height(value:number) {
this._height = value;
if(this._scaleMode==egret.StageScaleMode.FIXED_WIDTH || this._scaleMode==egret.StageScaleMode.FIXED_HEIGHT ) {
this._iframe.height=this._height/this._stageH*this._windowH+"px";
this._iframeWrapper.style.height=this._height/this._stageH*this._windowH+"px";
}
if(this._scaleMode==egret.StageScaleMode.SHOW_ALL || this._scaleMode==egret.StageScaleMode.NO_BORDER ) {
if(this._windowH==this._displayH){
this._iframe.style.height = this._height / this._stageH * this._windowH + "px";
this._iframeWrapper.style.height = this._height / this._stageH * this._windowH + "px";
}else{
this._iframe.style.height = this._height / this._stageH * this._displayH + "px";
this._iframeWrapper.style.height = this._height / this._stageH * this._displayH + "px";
}
}
}
public set x(value:number) {
this._x = value;
if(this._scaleMode==egret.StageScaleMode.FIXED_WIDTH || this._scaleMode==egret.StageScaleMode.FIXED_HEIGHT) {
this._iframeWrapper.style.left = this._x / this._stageW * this._windowW + "px";
}
if(this._scaleMode==egret.StageScaleMode.SHOW_ALL || this._scaleMode==egret.StageScaleMode.NO_BORDER ) {
if(this._windowW==this._displayW){
this._iframeWrapper.style.left = this._x / this._stageW * this._windowW + "px";
}else{
this._iframeWrapper.style.left = this._x / this._stageW * this._displayW + "px";
}
}
}
public set y(value:number) {
this._y = value;
if(this._scaleMode==egret.StageScaleMode.FIXED_WIDTH || this._scaleMode==egret.StageScaleMode.FIXED_HEIGHT ) {
this._iframeWrapper.style.top = this._y / this._stageH * this._windowH + "px";
}
if(this._scaleMode==egret.StageScaleMode.SHOW_ALL || this._scaleMode==egret.StageScaleMode.NO_BORDER){
if(this._windowH==this._displayH){
this._iframeWrapper.style.top = this._y / this._stageH * this._windowH + "px";
}else{
this._iframeWrapper.style.top =this._y / this._stageH * this._displayH + "px";
}
}
}
public get x():number {
return this._x;
}
public get y():number {
return this._y;
}
public get src():string {
return this._src;
}
public set src(value:string) {
this._src = value;
}
}
使用方式:
var webview=new WebView("http://www.sina.com");
webview.x=100;
webview.y=100;
webview.width = 500;
webview.height = 800;
webview.show();
效果:
- 后续
考虑屏幕大小改变、屏幕旋转以及单页面多Webplay实例的情形
————————————————
版权声明:本文为CSDN博主「小样儿0611」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xiaoyang0611/article/details/49128077