在cocos2d-js中实现自动绑定cocostudioUI控件与事件

一.起因
在客户端游戏开发中最让人恶心的工作就是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);

```

上面是最为直接访问控件的方法。问题在于如果一个UI界面中有十个、二十个甚至更多的UI控件需要操作的是候,我们做UI的界面、逻辑开发的同学们是否还有时间愉快的玩耍?有没有什么方法不需要去手写这些代码,就能愉快的访问UI组件与接收UI事件响应呢?
二. 思考
        对于手机游戏,特别是卡牌类的游戏来说70%~80%的客户端工作量是在UI布局与逻辑上。
       上面的seekWidgetByName、getChildByName、addTouchEventListener函数将大量充斥在客户端代码中,滥竽充数着我们的代码行数。

       自己曾经有过Qt的开发经验。Qt中也有自己的UI设计工具,生成xml的ui配置文件. 对于这个xml有两种使用方式:
       第一种方式: 使用Qt自己的编译工具,将xml翻译生成一个c++代码文件,代码内容就是根据xml中的信息创建各种控件,设置坐标\属性\事件等。
       第二种方式: 在程序中,将xml文件使用UILoader工具类加载进来,成为一个节点。然后调用Qt的函数实现信号/槽的自动关联。实现信号/槽(事件)自动绑定的原理,是要求写一个事件处理函数,格式为:  
void on_控件名_信号名(参数); 

```

具体如何使用就不细说了,有兴趣的朋友可以自己去看看。根据Qt的这个功能提示,我们何尝不可以在cocos2d-js自动绑定coccostudio输出的ui文件呢?
三. 名命约定

1. 代码名命约定
 根据cocos2d-js代码风格,我们约定:
        (1)类成员变量以下划线 "_"开头后面接以驼峰名命格式的英文单词。
 例如:_loginButton 、 _closeButton 、 _nameLabel
        (2)类中的私有函数也使用同样的方式。
         例如:_onLoginButtonTouchBegan: function() { ...}
2. UI命名约定
       在cocostudioUI编辑器中,我们遵循上述代码中成员变量的命名规范。将需要由代码访问的控件取名为"_"开头,后面接以驼峰名命格式的英文单词。 请看下图:


![](http://img.blog.csdn.net/20150106144346410)



3. ccui控件事件命名
ccui.Widget事件注册有两种:
1). 常规的touch事件有:
 ccui.Widget.TOUCH_BEGAN   触摸开始 (按下)
 ccui.Widget.TOUCH_MOVED  触摸移动 (移动)
 ccui.Widget.TOUCH_ENDED  触摸结束 (抬起)
 ccui.Widget.TOUCH_CANCELED  触摸取消 (一般没用)
 我们使用widget.addTouchEventListener(selector, target)给控件注册触摸事件,设置回调函数.


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)来注册。
 这里的selector就是我们的回调函数,需要我们取名并实现这个函数,事件类型是通过参数来识别的:
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, events: sz.UILoader.touchEvents},
    //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, events: sz.UILoader.touchEvents},
    //BMFont
    {widgetType: ccui.TextBMFont, events: sz.UILoader.touchEvents},
    //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注册事件响应。
6). 加载类接收到事件响应,转发事件到对应的target的事件处理函数上。
3.UI加载类的具体实现
 完整代码可以到githut下载: https://github.com/ShawnZhang2015/UILoader


五.前缀的问题
上面花了大量文字讲解了关于命名的问题,有人可能会觉得使用这个UILoader会强奸他的代码。
因为他没有使用“_”做为成员变量前缀,或是成员变量前缀不是“_”而是“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强奸事件的发生,可以兼容你原来的代码。




在cocos2d-js中实现控件的长按事件的常规思路 


1. 在控件touchBegan时,使用 一次性定时器scheduleOnce传入touchLong函数,设定1秒后执行。
2. touchLong函数触发后招待你的业务逻辑代码
2. 在touchEnded时执行unschedule函数,关闭定时器函数。防止长按时间不足时误触发了touchLong事件。
 

一.TouchLong事件
1.事件命名:与其它标准触摸事件一样使用: 前缀 + 控件名 + TouchLong TouchLong事件默认触发时间为1秒,你可以修改sz.UILoader.DEFAULT_TOUCH_LONG_TIME来设置默认触发时间。 
2.你也可以通过uiloader的TouchBegan事件返回一个大于0小于5的数字做为TouchLong的触发时间。 
3.TouchLong可以任意使用,不实现TouchBeang\TouchEnded也不影响。
4.TouchLong也可以配合TouchEvent下使用(TouchEvent为cocos2d标准事件响应函数以type区分是touchBegan/touchMoved/touchEnded,  TouchLong不会出现在其中,需要单独定义函数。)



![](http://cdn.cocimg.com/bbs/attachment/Fid_59/59_29719_7ec5daf1436b274.png)
 


二.绑定手动创建的ccui控件
1.sz.uiloader.widgetFromJsonFile(target, jsonFile)函数同时可以绑定target上的按命名约定的ccui控件。
2.ccui控件需要提前创建并设置名字, addChild到target上。
3.sz.uiloader.widgetFromJsonFile(target) 无需要json文件参数也能使用。


教程: http://blog.csdn.net/6346289/article/details/42453479
源码下载https://github.com/ShawnZhang2015/UILoader

楼主这个想法很好啊。给力。非常适合要做游戏的开发者使用的框架。

放到一楼 :5:

好东西,mark一下

楼主的代码不错,我根据自己需要做了下修改


var $utils = {
    bindEventHandler: function(root, target){
        var name = root.getName();
        if(name.charAt(0) == "_"){
            var prefix = "_on" + name.toUpperCase() + name.slice(2);
            
             {type:ccui.Button, events: "TouchBegan", "TouchMoved", "TouchEnded"]}
             ].forEach(function(typeEvents){
                 if(root instanceof typeEvents.type){
                     var eventHandler = typeEvents.events.map(function(event){
                         var func = this;
                         return typeof func === 'function' ? func : null;
                     },this);

                     if(eventHandler.some(function(f){return f;}))
                     {
                         root.addTouchEventListener(function(sender, type){
                             var handler = eventHandler;
                             if(handler) return handler.call(this, sender, type);
                         },this);
                     }
                 }
             },target);
        }
        if(root.getChildrenCount() > 0){
            root.getChildren().forEach(function(child){
                $utils.bindEventHandler(child, this);
            }, target);
        }
        
        return root;
    }
};

UILoader新增功能:

  1. 绑定手动创建的ccui控件
  ctor: function() {
      var image = new ccui.ImageView("xxx.png", ccui.Widget.LOCAL_TEXTURE);
      image.name = "_image";                      //必须设置名字
      this.addChild(image);                           //必须addChild到this上
      sz.uiloader.widgetFromJsonFile(this);  //不用json文件也行
      cc.assert(this. _image.name === "_ image");
  },

 _onImageTouchBegan: function() {
    ....
 }


```



2. 支持TouchLong事件

TouchLong事件,默认为按下1秒后触发。
你可以在TouchBegan事件提供一个返回值来设置TouchLong的触发时间。
_onImageTouchLong: function(sender) {
.... 
}

```





有兴趣的朋友请多顶一下,我测试后提交到githut

非常感谢楼主大大的分享,因为需要用到cocosStudio2.1版本所以对大大的做了一些修改,下面是我改的兼容cocosStudio2.1版本的

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;
this.version = options.version || 1;
if(this.version == 1) {
var rootNode = ccs.uiReader.widgetFromJsonFile(jsonFile);
}else{
var rootNode = ccs.csLoader.createNode(jsonFile);
}
if (!rootNode) {
cc.log(“Load json file failed”);
}
target.rootNode = rootNode;
target.addChild(rootNode);
this._bindMenbers(rootNode, target);

    //PS:原UILoader没给rootNode绑定事件,同时也没返回值
    this._registerWidgetEvent(target, rootNode);
    return rootNode;
},

非常感谢你的参与,本想将代码合并到一起,细看你修改的代码时,发现了一些小问题。
我花了一天时间,修改了sz.UILoader代码,增加了新版本cocostudio的支持,包括UI嵌套事件的支持,
提供了简单的测试代码。同时重写了cc.Node的事件注册函数。

相关修改如下图所示:

由于我并没有使用cocostudio2做项目,并没有严格测试代码,有兴趣朋友可以体验一下,有问题欢迎指正,再次感谢。

:2: 这个可以的。nice呀。mark下