3. 框架设计
本节我们要开发一个UI框架,底层以白鹭引擎为例。
框架设计的第一步并不是直接撸代码,而是先想清楚设计思想,抽象。
一个一个的UI窗口是独立的吗?不是的,因为它有层级关系,A窗口在B窗口下面,新创建的C窗口在最上面。因为它们直接有关系,所以它们需要被管理。另外一个原因是:统一的管理有利于机制的实现。举个例子,我们希望在大部分窗口打开的时候都有一个从小变大的效果,那么我们可以通过窗口管理器提供机制去支持这个需求,并且只要改动少量的不需要的这个特性的窗口的代码就可以。这边我们说清楚了为什么要做管理,接下来我们讨论的是管理是不是一个普遍行为。
管理发生不仅仅发生在代码中,也发生在我们生活的方方面面。老板管理高层员工,高层管理中层,中层管理下层员工,这都是管理。同样的,在代码里面,也有很多管理。窗口管理器管理窗口,场景管理器管理场景,所以这种管理就是一种高度的抽象,可以抽象成一个通用的机制。我们现在谈的UI框架,实际上就是一种管理思维,它可以作为窗口管理器管理窗口,也可以作为场景管理器管理场景。我们先过一眼这个框架:
UIManager.ts
class UIManager extends Base{
private tUIObjs:Object = {};
private oCanvas:egret.DisplayoUIObjectContainer;
public constructor(oCanvas:egret.DisplayoUIObjectContainer) {
this.oCanvas = oCanvas;
}
//////////////// 子类关注接口 //////////////////
//ui对象准备开始加载
protected onUIObjBeginLoad(oUIObj:UIView){
}
//私有接口
private destroyoUIObj(oUIObj:UIView){
oUIObj.onPreDestroy();//新增的
oUIObj.onDestroy();
this.oCanvas.removeChild(oUIObj);
}
private loadUIObj(oUIObj:UIView){
let sSkinName = oUIObj.skinXMLName();
if(sSkinName === undefined){//不需要加载皮肤
this.uiSkinReady(oUIObj);
}
else{
oUIObj.addEventListener(Event.SkinReady,
this.uiSkinReadyEvent, this);
oUIObj.loadSkin();
}
}
private uiSkinReadyEvent(event:Core.Event){
this.uiSkinReady(event.data);
}
private uiSkinReady(oUIObj:UIView){
this.oCanvas.addChild(oUIObj);
this.onUIObjBeginLoad(oUIObj);
oUIObj.setLoaded();
oUIObj.registeEventMap();
oUIObj.onPreLoaded();
oUIObj.onLoaded();
oUIObj.onEndLoaded();
}
/////////////// 对外接口 //////////////////
//窗口创建接口
public createUI(sClassname:string, ...args:oUIObject[]):UIView {
let oUIObj = <UIView>this.tUIObjs[sClassname];
if(oUIObj){
return oUIObj;
}
let oClassType:any = egret.getDefinitionByName(sClassname);
oUIObj = new oClassType(args);
this.tUIObjs[sClassname] = oUIObj;
oUIObj.setName(sClassname);
oUIObj.onCreate();
this.loadUIObj(oUIObj);
return oUIObj;
}
public destroyUI(sClassname:string){
let oUIObj = <UIView>this.tUIObjs[sClassname];
if (!oUIObj) {
console.warn("DestroyUI not exist sClassname:" +
sClassname);
return;
}
this.destroyoUIObj(oUIObj);
delete this.tUIObjs[sClassname];
}
public destroyAllUI(){
for(let uiName in this.tUIObjs){
let oUIObj = <UIView>this.tUIObjs[uiName];
this.destroyoUIObj(oUIObj);
}
this.tUIObjs = {}
}
}
UIView.ts
class UIBase extends eui.Component {
private bLoaded:boolean = false;
private bDestroyed = false;
public constructor() {
super();
}
///////////// 子类关注的接口 //////////////
//窗口对象创建时
public onCreate(){}
//所有东西都加载完成
public onLoaded() { }
//销毁时
public onDestroy() { }
//事件映射表
public eventMap() {
return undefined;
}
//皮肤名称
public skinXMLName():string{
return undefined;
}
/////////// 框架子类关注的接口 ///////////
//准备加载
public onPreLoaded() {
}
public onEndLoaded(){
}
/////////// 框架使用的接口 //////////////
public setLoaded(){
this.bLoaded = true;
}
//皮肤加载接口
public loadSkin(){
this.addEventListener(eui.UIEvent.COMPLETE, this.onSkinComplete,this);
this.skinName = "resource/eui_skins/view/" + this.skinXMLName() +".exml";
}
private onSkinComplete(){
this.removeEventListener(eui.UIEvent.COMPLETE,
this.onSkinComplete, this);
let oSkinEvent = egret.Event.create(Event, Event.SkinReady, false);
oSkinEvent.data = this;
this.dispatchEvent(oSkinEvent);
egret.Event.release(oSkinEvent);
}
public registeEventMap(){
let tEventMap = this.eventMap();
if(tEventMap == undefined){
return;
}
for (var i = 0; i < tEventMap.length; ++i) {
let tEventInfo = tEventMap[i];
let oComponent = <eui.Component>tEventInfo[0];
let fFunc:Function = <Function>tEventInfo[1];
let sEventType:string= <string>tEventInfo[2] ||egret.TouchEvent.TOUCH_TAP;
oComponent.addEventListener(sEventType, fFunc, this);
}
}
public setDestroy(){
this.bDestroyed = true;
}
//对外接口
public isValid(){
return !this.bDestroyed;
}
//私有接口
private isLoaded(){
return this.bLoaded;
}
private isDestroyed(){
return this.bDestroyed;
}
}
这个框架的基础部分的接口设计使用前面介绍过的设计方式。总的设计逻辑是这样的:
收集资源加载所必须的api,确定是同步加载还是异步加载。这边使用的loadskin是异步加载。
封装基础时机。基础时机在这边指的是一个被管理对象的创建和销毁以及异步加载完成。最本质的是创建以及销毁,及onCreate和onDestroy接口。异步加载完成时机是因为我们选用了异步加载而额外多出的时机(onLoaded)。在加载资源的各个阶段将这个”消息”合理的告诉子类,对于管理器而言,它告诉了子类准备开始加载子对象(onUIObjBeginLoad),而被管理者UIView告诉子类很多信息,比如onCreate,onDestroy,onLoaded。
加强被管理器的基础功能,抽象出配置化编程基础。
添加自动化平衡处理,防止逻辑错误。
这边解释下上面的点,onCreate和onDestroy在机制这层是一定保证对称出现的。即使继承类出现了逻辑异常,也需要保证它们都会各执行1次。配置化编程是这里面的设计的另外一个核心点,配置化编程指的是我们写代码的时候采用的就像配置一样的方式编码,任何冗余的代码都不需要书写。举个例子,如果你需要监听按钮A和按钮B的两个事件,你这么写:
this.oBtnA.addEventListener(egret.Event.TouchEvent, this.clickBtnA, this);
this.oBtnB.addEventListener(egret.Event.TouchEvent, this.clickBtnB, this);
这里面不同的是oBtnA,clickBtnA 与oBtnB,clickBtnB,其他都是相同的,所以我们用配置化编程改一下,变成了,我们的子类只需要实现:
public eventMap() {
return [
["oBtnA", "clickBtnA" ],
["oBtnB", "clickBtnB" ],
];
}
ok,我们的重复代码就消除了,这就是配置化编程的核心处理。
自动化平衡处理指的是在一个被管理者的生命周期,它要保证在最后能把该释放的都释放了,防止逻辑层泄露。举个例子,一个ui里面使用了一个定时器,通常的写法是在onDestroy里面判断存在就去销毁它,但是很多程序可能会忘记释放。那么这时候我们需要封装定时器管理对象,在准备销毁前去调用该对象的清理函数,保证所有的定时器都能被正确销毁。
这个定时器对象在创建定时器后都会保存住创建后的句柄,如果逻辑层没销毁,那么最后机制会统一帮它清理。自动化平衡处理还能干一些事,比如说异步加载资源回来,逻辑层不需要再去判断窗口是否销毁,回调的接口一定是保证了窗口存在的情况下才调用的。这同样也需要封装一个异步资源管理器对象来实现。
下面我们来看4个东西:
基于上面的代码,如何封装游戏项目的场景管理器和窗口管理器。
实现一个自动化平衡处理。
实现一个窗口
感受机制的威力
封装场景管理器
class SceneManager extends UIManager {
private oCurScene:SceneBase;
public constructor(canvas:egret.DisplayObjectContainer) {
super(canvas);
}
//子类框架实现接口
protected onUIObjBeginLoad(oUIObj:UIView){
if(this.oCurScene != undefined){
this.destroyScene(this.oCurScene.getName());
}
this.oCurScene = <SceneBase>oUIObj;
}
//对外接口
public createScene(sClassname:string, ...args:Object[]):SceneBase{
return <SceneBase>this.createUI(sClassname, ...args);
}
public destroyScene(sClassname:string){
return this.destroyUI(sClassname);
}
public createWindow(sClassname:string, ...args:Object[]):WindowBase{
Core.assert(this.oCurScene != undefined);
return <WindowBase>this.oCurScene.createWindow(sClassname, ...args);
}
public destroyWindow(sClassname:string){
Core.assert(this.oCurScene != undefined);
this.oCurScene.destroyWindow(sClassname);
}
public getWindow(sClassname:string):WindowBase{
return this.oCurScene.getWindow(sClassname);
}
public getCurScene():SceneBase{
return this.oCurScene;
}
}
封装场景:
class SceneBase extends UIView {
private oUIMgr:UIManager;
//子类框架实现接口
public onPreLoaded(){
this.oUIMgr = new UIManager(this);
}
public createWindow(classname:string, ...args:Object[]):WindowBase{
return <WindowBase>this.oUIMgr.createUI(classname, ...args);
}
public destroyWindow(classname:string){
return this.oUIMgr.destroyUI(classname);
}
public getWindow(classname:string):WindowBase{
return <WindowBase>this.oUIMgr.getUI(classname);
}
}
封装窗口:
class WindowBase extends UIView {
public constructor() {
super();
}
//子类框架实现接口
public onPreLoaded() {
//窗口从小变大效果
}
}
上面封装的一些缘由:场景同时只能存在一个,窗口需要一些自定义的效果。因为有了这些特殊性,所以延伸了它们的子类。上面还有一个地方特别要注意,子类关注的东西如果被继承类实现了,那么这个接口在它子类这个级别就变成了框架子类关注,因为子类要在实现的时候调用父类同名接口了。
下面来看一个自动化平衡处理,处理下定时器。
先加强下框架的功能:
在UIManager的代码中加一个代码
//私有接口
private destroyoUIObj(oUIObj:UIView){
oUIObj.onPreDestroy();//新增的
oUIObj.onDestroy();
this.oCanvas.removeChild(oUIObj);
}
在UIView的框架子类关注的接口中加一个
public onPreDestroy(){
}
TimeHelp的实现:
class TimeHelp {
private tHandlers:Array<TimerHandler> = new Array<TimerHandler>;
public addTimeHandle(){
let oTimerHandler = ; //调用引擎时间管理器接口返回oTimerHandler
tHandlers.Add(oTimerHandler);
return oTimerHandler;
}
public rmTimeHandle(oTimerHandler){
if(oTimerHandler.isValid()){
//调用引擎时间管理器接口移除oTimerHandler
}
}
public clear(){
let tHandlers = this.tHandlers;
for(let i = 0; i < tHandlers.length; ++i){
let oTimerHandler = tHandlers[i];
if(oTimerHandler.isValid()){
//调用引擎时间管理器接口移除oTimerHandler
}
}
this.tHandlers.clear();
}}
最后在windowbase里面集成:
class WindowBase extends UIView {
private oTimeHelp:TimeHelp = new TimeHelp();
public constructor() {
super();
}
//子类框架实现接口
public onPreLoaded() {
//窗口从小变大效果
}
public addTimeHandle(...){
//调用系统时间管理器接口返回oTimerHandler
return oTimeHelp.addTimeHandle();
}
public rmTimeHandle(oTimerHandler){
oTimeHelp.rmTimeHandle(oTimerHandler);
}
public onPreDestroy(){
oTimeHelp.clear();
}
}
集成完毕,一切都很安静,这就是防止逻辑犯错的平衡。而UI逻辑层的都调用新封装的接口而不是直接调用底层接口,这样就避免的犯错的可能。
接下来我们看一个窗口的实现,非常简单:
class SettingPanel extends UIView{
private oBtnUIEffect:eui.Image;
private oBtnBg:eui.Image;
//事件映射表
public eventMap() {
return [
["oBtnUIEffect", "changeUIEffect"],
["oBtnBg", "changeBg"]
];
}
//皮肤名称
public skinXMLName():string{
return undefined;
}
public onLoaded() {
//显示默认设置
}
private changeUIEffect(){
//切换ui音效开关
}
private changeBg(){
//切换背景音乐开关
}
}
干干净净,舒舒服服,代码很清晰。
上面主要是讲我们对于我们的使用者,逻辑UI的编写者提供了很大的便利,以及防止他们的一些逻辑错误,但是它的威力远远不止如此。
我们假定我们现在开发了很多个系统,突然策划提出我们需要在按钮的点击响应上面播放点击音效。这时候有个做法是在每个事件响应的回调里面添加一句播放的代码。但是,自从我们有了机制,有了对消息的拦截,我们可以在事件响应的地方做拓展。将UIView的registeEventMap拓展下:
public registeEventMap(){
let tEventMap = this.eventMap();
if(tEventMap == undefined){
return;
}
for (var i = 0; i < tEventMap.length; ++i) {
let tEventInfo = tEventMap[i];
let oComponent = <eui.Component>tEventInfo[0];
let fFunc:Function = <Function>tEventInfo[1];
let sEventType:string= <string>tEventInfo[2] ||
egret.TouchEvent.TOUCH_TAP;
oComponent.addEventListener(sEventType, this.eventHook.bind(fFunc, this),this, fFunc);
}
}
private eventHook(fCallFunc){
//播放声音
fCallFunc();
}
可以看到,一旦有了机制,我们就可以实现全局性的功能,尤其擅长处理大规模需要重复性代码的东西。这也是需要所有窗口都继承自UIView,且由UIManager创建的意义。