一.起因
在客户端游戏开发中最让人恶心的工作就是UI相关的东西,虽然有了像cocostudio这样的可视化工具,但界面中有大量需要由代码访问的控件的时候,需要写太多重复的代码例如:
//加载UI配置文件 var root = ccs.uiReader.widgetFromJsonFile("res/cocosui/UIEditorTest/UIButton_Editor/UIButton_Editor_1.json"); this._mainNode.addChild(root); //查询back控件,并添事件响应 var back_label = ccui.helper.seekWidgetByName(root,"back"); back_label.addTouchEventListener(this.backEvent,this); //查询Button_123控件,并添事件响应 var button = root.getChildByName(root,"Button_123"); button.addTouchEventListener(this.touchEvent,this);</span>
上面是最为直接访问控件的方法。问题在于如果一个UI界面中有十个、二十个甚至更多的UI控件需要操作的是候,我们做UI的界面、逻辑开发的同学们是否还有时间愉快的玩耍?有没有什么方法不需要去手写这些代码,就能愉快的访问UI组件与接收UI事件响应呢?
二. 思考
对于手机游戏,特别是卡牌类的游戏来说70%~80%的客户端工作量是在UI布局与逻辑上。
第二种方式: 在程序中,将xml文件使用UILoader工具类加载进来,成为一个节点。然后调用Qt的函数实现
信号/槽的自动关联。实现信号/槽(事件)自动绑定的原理,是要求写一个事件处理函数,格式为:
void on_控件名_信号名(参数);
三.
名命约定
1. 代码名命约定
根据cocos2d-js代码风格,我们约定:
(1)类成员变量以下划线 "_"开头后面接以驼峰名命格式的英文单词。
例如:_loginButton 、 _closeButton 、 _nameLabel
(2)类中的私有函数也使用同样的方式。
例如:_onLoginButtonTouchBegan: function() { ...}
2. UI命名约定
3. ccui控件事件命名
ccui.Widget事件注册有两种:
1).常规的touch事件有:
ccui.Widget.TOUCH_BEGAN 触摸开始 (按下)
ccui.Widget.TOUCH_MOVED 触摸移动 (移动)
ccui.Widget.TOUCH_ENDED 触摸结束 (抬起)
ccui.Widget.TOUCH_CANCELED 触摸取消 (一般没用)
ccui.Widget.TOUCH_CANCELED 触摸取消 (一般没用)
2). 控件特殊事件:
比如CheckBox的:
ccui.CheckBox.EVENT_SELECTED
ccui.CheckBox.EVENT_UNSELECTED
又如:TextField的:
ccui.TextField.EVENT_ATTACH_WITH_IME
ccui.TextField.EVENT_DETACH_WITH_IME
ccui.TextField.EVENT_INSERT_TEXT
ccui.TextField.EVENT_DELETE_BACKWARD
这类事件需要使用widget.addEventListener(selector,target)来注册。
ctor: function() { this._super(); ... button.addTouchEventListener(this._onButtonEvent,this); },_onButtonEvent: function(sender,type) { switch(type) { case ccui.Widget.TOUCH_BEGAN: ...; return true; case ccui.Widget.TOUCH_MOVED: ...; break; case ccui.Widget.TOUCH_ENDED: ...; break; } }
这个"_onButtonEvent"就是我们为事件函数取的名字,如果我们按:【 前缀+控件名(取掉下划线)+事件名】给控件事件函数取名
举例说明:
控件名字为:_
button
事件名则为:_onButtonTouchBegan、_onButtonTouchMoved、_onButtonTouchEnded
四. 代码实现
有了上面的约定,我们可以开始实现UI的绑定了。
1. 定义一个自动绑定的控件列表,我们这里列出了常用的控件类型与事件名字。
//触摸事件 sz.UILoader.touchEvents = ["TouchBegan","TouchMoved","TouchEnded"]; //控件事件列表 sz.UILoader.widgetEvents = [ //Button {widgetType: ccui.Button,events: sz.UILoader.touchEvents},//ImageView {widgetType: ccui.ImageView,//TextFiled {widgetType: ccui.TextField,events: ["AttachWithIME","DetachWithIME","InsertText","DeleteBackward"]},//CheckBox {widgetType: ccui.CheckBox,events: ["Selected","Unselected"]},//ListView {widgetType: ccui.ListView,events:["SelectedItem"]},//Panel {widgetType: ccui.Layout,//BMFont {widgetType: ccui.TextBMFont,//last must null null ];
这个sz.UILoader.widgetEvents数组可以根据需要自己添加需要绑定的组件。
2.逻辑流程
1). 使用loader载入ui文件并传入target为当前layer。所有事件和控件变量都将绑定到target上。
2). 遍历载入后的子节点(childNode),检查名字前缀是否以”_”开头。并且该节点类型是否在widgetEvents数组中。
3). 将childNode绑定到target上。
4). 提取childNode事件函数名,检查target是否有这些函数存在。
5). 为widgetNode注册事件响应。
3). 将childNode绑定到target上。
4). 提取childNode事件函数名,检查target是否有这些函数存在。
5). 为widgetNode注册事件响应。
6). 加载类接收到事件响应,转发事件到对应的target的事件处理函数上。
3.UI加载类的具体实现
sz.UILoader = cc.Class.extend({ _eventPrefix: null,_memberPrefix: null,/** * 加载UI文件 * @param target将 jsonFile加载出的节点绑定到的目标 * @param jsonFile cocostudio UI编辑器生成的json文件 */ widgetFromJsonFile: function(target,jsonFile,options) { cc.assert(target && jsonFile); if (!options) { options = {}; } this._eventPrefix = options.eventPrefix || sz.UILoader.DEFAULT_EVENT_PREFIX; this._memberPrefix = options.memberPrefix || sz.UILoader.DEFAULT_MEMBER_PREFIX; var rootNode = ccs.uiReader.widgetFromJsonFile(jsonFile); if (!rootNode) { cc.log("Load json file Failed"); } target.rootNode = rootNode; target.addChild(rootNode); this._bindMenbers(rootNode,target); },/** * 递归对rootWidget下的子节点进行成员绑定 * @param rootWidget * @param target * @private */ _bindMenbers: function(rootWidget,target) { var widgetName,children = rootWidget.getChildren(); var self = this; children.forEach(function(widget) { widgetName = widget.getName(); //控件名存在,绑定到target上 var prefix = widgetName.substr(0,self._memberPrefix.length); if (prefix === self._memberPrefix) { target[widgetName] = widget; self._registerWidgetEvent(target,widget); } //绑定子控件,可以实现: a._b._c._d 访问子控件 if (!rootWidget[widgetName]) { rootWidget[widgetName] = widget; } //如果还有子节点,递归进去 if (widget.getChildrenCount()) { self._bindMenbers(widget,target); } }); },/** * 获取控件事件 * @param widget * @returns {*} */ _getWidgetEvent: function(widget) { var bindWidgetEvent = null; var events = sz.UILoader.widgetEvents; for (var i = 0; i < events.length; i++) { bindWidgetEvent = events[i]; if (widget instanceof bindWidgetEvent.widgetType) { break; } } return bindWidgetEvent; },/** * 注册控件事件 * @param target * @param widget * @private */ _registerWidgetEvent: function(target,widget) { var name = widget.getName(); //截取前缀,首字母大写 var newName = name[this._memberPrefix.length].toUpperCase() + name.slice(this._memberPrefix.length + 1); var eventName = this._eventPrefix + newName + "Event"; var isBindEvent = false; if (target[eventName]) { isBindEvent = true; } else { //取事控件件名 var widgetEvent = this._getWidgetEvent(widget); if (!widgetEvent) { return; } //检查事函数,生成事件名数组 var eventNameArray = []; for (var i = 0; i < widgetEvent.events.length; i++) { eventName = this._eventPrefix + newName + widgetEvent.events[i]; eventNameArray.push(eventName); if (cc.isFunction(target[eventName])) { isBindEvent = true; } } } //事件响应函数 var self = this; var eventFunc = function(sender,type) { var callBack; if (eventNameArray) { var funcName = eventNameArray[type]; callBack = target[funcName]; } else { callBack = target[eventName]; } if (self._onWidgetEvent) { self._onWidgetEvent(sender,type); } if (callBack) { return callBack.call(target,sender,type); } }; //注册事件监听 if (isBindEvent) { widget.setTouchEnabled(true); if (widget.addEventListener) { widget.addEventListener(eventFunc,target); } else { widget.addTouchEventListener(eventFunc,target); } } } }); sz.uiloader = new sz.UILoader();
所有东西都已经准备好了,我们看看怎么在具体的代码中使用:
GameLayer = cc.Layer.extend({ ctor: function() { this._super(); //加载UI文件,绑定控件和事件到this sz.uiloader.widgetFromJsonFile(this,"res/DemoLogin.ExportJson"); //立即可以访问控件的属性方法了 cc.log(this._closeButton.getName()); },/* * _closeButton的TouchBegan事件处理函数 */ _onCloseButtonTouchBegan: function(sender) { cc.log("_onCloseButtonTouchBegan"); },});
现在看看我的客户端代码是不是比之前的简洁多了!
五.前缀的问题
因为他没有使用“_”做为成员变量前缀,或是成员变量前缀不是“_”而是“m_”。
为了不强奸别人的代码,提供了下面的选项:
sz.uiloader.widgetFromJsonFile(this,"res/DemoLogin.ExportJson",{eventPerfix:"on",memberPrefix:"m_"} );最后一个可选参数options对象,有两个属性 eventPerfix 、memberPrefix用于配置事件前缀和成员变量前缀
六. 前缀+控件名+Event
有些时候,不想将TouchBegan、TouchMoved、TouchEnded分成三个响应函数分别来写,而是使用原来事件参数来判断事件类型。
这时你只需要实现以【前缀+控件名+Event】的函数名例如:
控件名为_loginButton, 定义一个函数如:
_onLoginButtonEvent: function(sender,type) { switch (type) { case 0: cc.log("_onLoginButtonEvent: began"); break; case 1: cc.log("_onLoginButtonEvent: move"); break; case 2: cc.log("_onLoginButtonEvent: end"); break; } },
这时UILoader会优先使用这个事件处理函数,如果同时也实现了一个"_onLoginButtonTouchBegan",它将不会被执行。
这样再次阻止sz.UILoader强奸事件的发生,可以兼容你原来的代码。
完整代码可以到githut下载:https://github.com/ShawnZhang2015/UILoader