Appearance
鼠标事件与触摸事件
在第一个前端工程中,我们看到了引擎初始化的过程,其中是会从document上获取canvas对象用于渲染,另外是获取一个element对象来响应鼠标事件的touchElement,如果没有额外的事件响应对象,那么这个对象就是用于渲染的canvas对象。
typescript
export const initEngine = (): Promise<boolean> => {
const rlt = new Promise<boolean>((resolve) => {
const urlSearch = new URLSearchParams(location.search);
const backend: string = urlSearch.get('backend') || 'webgl';
registerSImpl();
RoyEngine.init(
['./assets/favicon.ico'],
() => {
const mainCanvas = document.getElementById('layout-canvas') as HTMLCanvasElement;
const touchCanvas = document.getElementById('layout-touch') as HTMLCanvasElement;
const elementData: ElementData = {
mainCanvas: mainCanvas,
touchCanvas: touchCanvas,
stretchCanvas: true,
hireachyPanel: document.getElementById('layout-hireachy'),
inspectorPanel: document.getElementById('layout-inspector'),
assetsPanel: document.getElementById('layout-assets'),
};
_GAppNativePC = new AppNativePC(elementData);
window['app'] = _GAppNativePC;
resolve(true);
},
backend,
);
});
return rlt;
};
该对象传入AppClient之后,会绑定鼠标事件,而无需在应用侧再次绑定,应用侧只需要按照一定的规则来响应事件即可。
typescript
this.render = this.render.bind(this);
this.resize = this.resize.bind(this);
window.addEventListener('resize', this.resize);
this.onMouseDown = this.onMouseDown.bind(this);
this.m_elementData.touchCanvas.addEventListener('mousedown', this.onMouseDown, false);
this.onMouseMove = this.onMouseMove.bind(this);
this.m_elementData.touchCanvas.addEventListener('mousemove', this.onMouseMove, false);
this.onMouseUp = this.onMouseUp.bind(this);
this.m_elementData.touchCanvas.addEventListener('mouseup', this.onMouseUp, false);
this.onMouseEnter = this.onMouseEnter.bind(this);
this.m_elementData.touchCanvas.addEventListener('mouseenter', this.onMouseEnter, false);
this.onMouseLeave = this.onMouseLeave.bind(this);
this.m_elementData.touchCanvas.addEventListener('mouseleave', this.onMouseLeave, false);
this.onTouchStart = this.onTouchStart.bind(this);
this.m_elementData.touchCanvas.addEventListener('touchstart', this.onTouchStart, true);
this.onTouchMove = this.onTouchMove.bind(this);
this.m_elementData.touchCanvas.addEventListener('touchmove', this.onTouchMove, true);
this.onTouchCancel = this.onTouchCancel.bind(this);
this.m_elementData.touchCanvas.addEventListener('touchcancel', this.onTouchCancel, true);
this.onTouchEnd = this.onTouchEnd.bind(this);
this.m_elementData.touchCanvas.addEventListener('touchend', this.onTouchEnd, true);
this.m_keyboard = new Keyboard(window);
this.onKeyDown = this.onKeyDown.bind(this);
this.m_keyboard.on('keydown', this.onKeyDown);
this.onKeyUp = this.onKeyUp.bind(this);
this.m_keyboard.on('keyup', this.onKeyUp);
this.onKeyPress = this.onKeyPress.bind(this);
this.m_keyboard.on('keypress', this.onKeyPress);
this.m_keyboard.registerHotkey({ key: 'KeyD', shiftKey: true, ctrlKey: true }, async (event: KeyboardEvent) => {
this.toggleFpsCounter();
event.preventDefault();
});
this.onMouseWheel = this.onMouseWheel.bind(this);
this.m_elementData.touchCanvas.addEventListener('wheel', this.onMouseWheel, false);
this.m_elementData.touchCanvas.addEventListener('contextmenu', (event) => {
event.preventDefault();
});
为了有序的分模块的响应鼠标事件,封装了MouseEventStragety类,它的一个实例被AppClient持有,绑定的事件都将送入到MouseEventStragety中。MouseEventStragety接受若干个用于响应具体事件的实例MouseEventStragetyIns。
注册实例对象,实例对象有优先级。
typescript
registerStragety(inst: MouseEventStragetyIns) {
if (this.m_stragetyMap[inst.name]) {
Trace.error('Twice Registered!');
return;
}
this.m_stragetyMap[inst.name] = inst;
this.m_priorityList.push(inst);
this.m_priorityList = this.m_priorityList.sort((a, b) => {
return a.priority < b.priority ? -1 : 1;
});
}
关注的响应策略函数是
- onTouchStart
- onTouchMove
- onTouchEnd
- onTouchCancel
- onMouseWheel
- onMouseDown
- onMouseMove
- onMouseUp
- onMouseEnter
- onMouseLeave
按照优先级响应事件,以mousedown为例,如果实例的onMouseDown为true,不再继续透传给其他实例。
typescript
onMouseDown(event: MouseEvent): boolean {
event.preventDefault();
this.m_mouseActFrameStamp = _GNowTimeMs;
if (event.target != this.canvas) {
return false;
}
this.m_mouseDownX = event.offsetX;
this.m_mouseDownY = event.offsetY;
this.m_shiftPress = event.shiftKey;
for (const ctrl of this.m_priorityList) {
if (ctrl.onMouseDown(event)) {
break;
}
}
return true;
}
目前已经内置了通用的响应实例,它们是
- MESCameraCtrl: 把事件传递给相机控制器,用来控制相机
- MESObjectSelect: 把事件向场景查询,用来拾取或选中物体
- MESObjectCtrl: 把事件传递给物体控制器,用来控制物体的移动旋转和缩放
这三个控制策略每个只做一件事情,且它们之间也有优先级排序,如MESObjectCtrl要高于其他二者,在旋转物体的时候,就不要旋转相机镜头了。
插件或者说项目侧,还可以根据自身需要扩展自定义的鼠标响应策略,并注册到MouseEventStragety中。根据项目的需要选择性的注册需要的响应策略。
值得注意的是,AppClient及其派生类是可以通过override的方式重载方法来响应到鼠标事件的,但是从框架协同的角度来讲更期望使用MouseEventStragety来处理事件。另外的一个好处是,有的时候并不是仅仅有一个画布,可能右上角还有一个小视口来显示其他内容,那么针对画布的事件处理,可以复用同样的一套逻辑。
最后,我们可以看一下触摸的处理。触摸部分主要是封装了多个触摸点,在MouseEventStragety中,已经封装了数据结构,来描述和跟踪各个触点的down、move、up、cancle事件。后续可以方便的接入FingerGesture这种手势的插件。