cocos2dx-CCControlButton源码分析

CCControlButtton

CCMenu简介

CCControlButtton是按钮控件,由一个标签加背景图片构成。cocos的CCMenu以及提供了相应的控件功能,CCMenu同CCControlButtton一样派生于CCLayer,可以开启触摸。CCMenu的孩子CCMenuItem已经提供了简单的按钮功能了,可以由图片或者文字构建一个菜单项,CCMenu可以看成一个控件层,在上面布局子控件CCMenuItem。下面是CCMenu的应用代码:

CCLabelTTF *label = CCLabelTTF::create("CCSequence test", "Arial", 24);
CCMenuItemLabel *item = CCMenuItemLabel::create(label, this, menu_selector(ActionScene::menuAction1));
item->setPosition(ccp(320, 1100));
menu->addChild(item);

上面在创建的时候直接指定了目标与动作,其中动作部分是menu_selector(ActionScene::menuAction1),menu_selector的作用是取ActionScene::menuAction1函数指针并向上转型,这里ActionScene必须是CCObject子类。相关代码如下:

typedef void (CCObject::*SEL_MenuHandler)(CCObject*);
#define menu_selector(_SELECTOR) (SEL_MenuHandler)(&_SELECTOR)

SEL_MenuHandler是void CCObject::fun(CCObject*)类型函数指针。
这里指定菜单项是一个标签,可以使用CCMenuItemSprite、CCMenuItemImage创建带有图片的菜单项。

可以先创建好菜单项,然后再指定目标动作,代码如下:

CCMenuItemImage *img = CCMenuItemImage::create("reset.png", "reset.png");
img->setTarget(this, menu_selector(ActionScene::reset));
img->setPosition(ccp(50, 1000));
menu->addChild(img);

菜单项的触摸事件比较简单,只有touchdown这个事件,无法处理touchdown后的手指拖拽。CCMenu是cocos标准库中的内容,后来cocos引入了其他人对cocos的扩展,其中有一个是控件扩展,提供的控件挺多,其中有一个控件是CCControlButtton,它同CCMenuItem一样,可以有标签和图片,图片使用CCScale9Sprite,支持九宫格图片。除此之外,CControlButtton定义了更多的事件,这些事件如下:

事件 描述
CCControlEventTouchDown 开始触摸
CCControlEventTouchDragEnter 触摸后从控件外拖拽进来
CCControlEventTouchDragInside 触摸后在控件内拖拽
CCControlEventTouchDragExit 触摸后从控件里拖拽出来
CCControlEventTouchDragOutside 触摸后在控件外拖拽
CCControlEventTouchUpInside 触摸后在控件里触摸结束
CCControlEventTouchUpOutside 触摸后在控件外触摸结束

上面看出CCControlButtton对手势的事件支持挺多的,这里主要根据用户的手在不在控件里进行手势划分的。
接下来对CCControlButtton进行详细的分析,目的除了了解使用背后的原理,更是为了方便自定义一些控件,构造游戏中的控件库。

CCControlButtton代码分析

CCControlButtton的应用代码如下:

CCSpriteFrame *btnSf = CCSpriteFrame::create("ui.png", CCRectMake(760, 163, 64, 60));
CCScale9Sprite *btnS9 = CCScale9Sprite::createWithSpriteFrame(btnSf);
CCControlButton *btn = CCControlButton::create(btnS9);
btn->setPreferredSize(CCSizeMake(64, 64));
btn->setPosition(ccp(100, 50));
btn->addTargetWithActionForControlEvents(this, cccontrol_selector(TilemapTest::itemsBarClick), CCControlEventTouchDown);
this->addChild(btn);

上面的代码添加了一个CCControlButton,我使用了一张网上download下来的游戏资源图片创建了一个精灵帧(描述纹理以及纹理的使用范围),然后用此创建一个九宫格精灵,再用它创建CCControlButton,setPreferredSize目的是防止九宫格精灵被变形,这里只想保持纹理的默认大小。由于网上并没有提供图片中每个子图片的属性描述文件,所以只能手动测量子图片在大图片中的的纹理范围了,这里范围是CCRectMake(760, 163, 64, 60)。最终在场景中创建了一个物品按钮,图片如下:

cocos2dx-CCControlButton源码分析_第1张图片

上面是一个RPG小游戏,黑色区域是碰撞区,占时没有隐藏。图片左下角是加入的按钮
现在分析CCControlButton是如何创建的,以及如何响应用户触摸事件的。CCScale9Sprite::createWithSpriteFrame(btnSf)代码如下:

CCControlButton* CCControlButton::create(CCScale9Sprite* sprite)
{
    CCControlButton *pRet = new CCControlButton();  //创建CCControlButton
    pRet->initWithBackgroundSprite(sprite); //初始化
    pRet->autorelease();
    return pRet;
}

pRet->initWithBackgroundSprite(sprite);代码如下:

bool CCControlButton::initWithBackgroundSprite(CCScale9Sprite* sprite)
{
    CCLabelTTF *label = CCLabelTTF::create("", "Arial", 30); //创建标签
    return initWithLabelAndBackgroundSprite(label, sprite); //继续初始化
}

initWithLabelAndBackgroundSprite(label, sprite);代码如下:

bool CCControlButton::initWithLabelAndBackgroundSprite(CCNode* node, CCScale9Sprite* backgroundSprite)
{
    if (CCControl::init())
    {
        CCAssert(node != NULL, "Label must not be nil.");
        CCLabelProtocol* label = dynamic_cast(node);  //向上转型用于下面检查
        CCRGBAProtocol* rgbaLabel = dynamic_cast(node); //向上转型用于下面检查
        CCAssert(backgroundSprite != NULL, "Background sprite must not be nil.");
        CCAssert(label != NULL || rgbaLabel!=NULL || backgroundSprite != NULL, "");

        m_bParentInited = true;

        // Initialize the button state tables
        this->setTitleDispatchTable(CCDictionary::create());  //用字典初始化4个表
        this->setTitleColorDispatchTable(CCDictionary::create());
        this->setTitleLabelDispatchTable(CCDictionary::create());
        this->setBackgroundSpriteDispatchTable(CCDictionary::create());

        setTouchEnabled(true); //开启触摸
        m_isPushed = false; //是否被按下
        m_zoomOnTouchDown = true; //按下时是否执行放大与缩小动作

        m_currentTitle=NULL; //标签内容为空

        // Adjust the background image by default
        setAdjustBackgroundImage(true); //调整背景图片
        setPreferredSize(CCSizeZero); //设置偏爱的大小
        // Zooming button by default
        m_zoomOnTouchDown = true;

        // Set the default anchor point
        ignoreAnchorPointForPosition(false);
        setAnchorPoint(ccp(0.5f, 0.5f));

        // Set the nodes
        setTitleLabel(node);
        setBackgroundSprite(backgroundSprite);

        // Set the default color and opacity
        setColor(ccc3(255.0f, 255.0f, 255.0f));
        setOpacity(255.0f);
        setOpacityModifyRGB(true);

        // Initialize the dispatch table

        CCString* tempString = CCString::create(label->getString());
        //tempString->autorelease();
        setTitleForState(tempString, CCControlStateNormal);
        setTitleColorForState(rgbaLabel->getColor(), CCControlStateNormal);
        setTitleLabelForState(node, CCControlStateNormal);
        setBackgroundSpriteForState(backgroundSprite, CCControlStateNormal);

        setLabelAnchorPoint(ccp(0.5f, 0.5f));

        // Layout update
        needsLayout();

        return true;
    }
    //couldn't init the CCControl
    else
    {
        return false;
    }
}

this->setTitleDispatchTable(CCDictionary::create()); 等四个函数就是设置4个表,分别对应标签文本、标签颜色、标签、背景图片。用户可以存储标签与图片的不同状态在这些表中,控件在每一个状态,就会使用此状态下的标签与图片。
setTouchEnabled(true); 开启触摸,这里将调用CCLayer的,因为CCControlButton是CCLayer子类,并且cocos2dx2.2.2也就CCLayer实现了向触摸事件分发器注册与移除触摸代理的功能,继承CCLayer就可以使用这些功能了。这里是cocos2dx的触摸机制的详细分析cocos2dx-2.2.2触摸详解
setAdjustBackgroundImage(true);代码如下:

void CCControlButton::setAdjustBackgroundImage(bool adjustBackgroundImage)
{
    m_doesAdjustBackgroundImage=adjustBackgroundImage;
    needsLayout();
}

上面调用到了needsLayout();它是对按钮进行重新布局的,代码如下:

void CCControlButton::needsLayout()
{
    if (!m_bParentInited) { //CCControl::init()调用后为true,不会进入
        return;
    }
    // Hide the background and the label
    if (m_titleLabel != NULL) {  //隐藏标签
        m_titleLabel->setVisible(false);
    }
    if (m_backgroundSprite) {  //隐藏背景
        m_backgroundSprite->setVisible(false);
    }
    // Update anchor of all labels  //设置标签锚点
    this->setLabelAnchorPoint(this->m_labelAnchorPoint);

    // Update the label to match with the current state
    CC_SAFE_RELEASE(m_currentTitle); //更新标签要用到的文字
    m_currentTitle = getTitleForState(m_eState);
    CC_SAFE_RETAIN(m_currentTitle);

    m_currentTitleColor = getTitleColorForState(m_eState);  //获得标签要用的颜色

    this->setTitleLabel(getTitleLabelForState(m_eState)); //设置按钮标签

    CCLabelProtocol* label = dynamic_cast<CCLabelProtocol*>(m_titleLabel);
    if (label && m_currentTitle)
    {
        label->setString(m_currentTitle->getCString()); //改变标签文字
    }

    CCRGBAProtocol* rgbaLabel = dynamic_cast<CCRGBAProtocol*>(m_titleLabel);
    if (rgbaLabel)
    {
        rgbaLabel->setColor(m_currentTitleColor); //改变标签颜色
    }
    if (m_titleLabel != NULL)
    {
        m_titleLabel->setPosition(ccp (getContentSize().width / 2, getContentSize().height / 2)); //设置标签位置
    }

    // Update the background sprite
    this->setBackgroundSprite(this->getBackgroundSpriteForState(m_eState)); //设置背景图片
    if (m_backgroundSprite != NULL)
    {
        m_backgroundSprite->setPosition(ccp (getContentSize().width / 2, getContentSize().height / 2)); //设置背景图片位置
    }

    // Get the title label size
    CCSize titleLabelSize;
    if (m_titleLabel != NULL)
    {
        titleLabelSize = m_titleLabel->boundingBox().size;  //获得标签变换后大小
    }

    // Adjust the background image if necessary
    if (m_doesAdjustBackgroundImage) //对图片进行调整
    {
        // Add the margins
        if (m_backgroundSprite != NULL)
        {
            //调整图片大小
            m_backgroundSprite->setContentSize(CCSizeMake(titleLabelSize.width + m_marginH * 2, titleLabelSize.height + m_marginV * 2));
        }
    }
    else
    {        
        //TODO: should this also have margins if one of the preferred sizes is relaxed?
        if (m_backgroundSprite != NULL)
        {
            CCSize preferredSize = m_backgroundSprite->getPreferredSize();
            if (preferredSize.width <= 0)//背景图片大小必须至少跟标签一样大
            {
                preferredSize.width = titleLabelSize.width;
            }
            if (preferredSize.height <= 0)
            {
                preferredSize.height = titleLabelSize.height;
            }

            m_backgroundSprite->setContentSize(preferredSize);
        }
    }

    // Set the content size
    CCRect rectTitle;
    if (m_titleLabel != NULL)
    {
        rectTitle = m_titleLabel->boundingBox();
    }
    CCRect rectBackground;
    if (m_backgroundSprite != NULL)
    {
        rectBackground = m_backgroundSprite->boundingBox();
    }

    //求标签与图片的范围交,然后设置为按钮大小
    CCRect maxRect = CCControlUtils::CCRectUnion(rectTitle, rectBackground);
    setContentSize(CCSizeMake(maxRect.size.width, maxRect.size.height));        

    //改变标签与图片位置,并让它们可见
    if (m_titleLabel != NULL)
    {
        m_titleLabel->setPosition(ccp(getContentSize().width/2, getContentSize().height/2));
        // Make visible the background and the label
        m_titleLabel->setVisible(true);
    }

    if (m_backgroundSprite != NULL)
    {
        m_backgroundSprite->setPosition(ccp(getContentSize().width/2, getContentSize().height/2));
        m_backgroundSprite->setVisible(true);   
    }   
}

上面代码主要是获得控件当前状态标签的文本与颜色,以及标签和背景图片。前面一开始隐藏然后最后又显示,可能之间状态存在标签、图片,这个状态标签图片都没有了,所以重新根据状态进行标签与图片布局的时候,一开始就隐藏,若此状态仍然存在标签与图片,则把它显示出来。这里注意的是初始化在后面调用setTitleLabel(node);setBackgroundSprite(backgroundSprite);设置标签与背景图片对象,所以这里面m_titleLabel与m_backgroundSprite都为NULL,也就只有m_doesAdjustBackgroundImage=adjustBackgroundImage这句代码做了事。
m_currentTitleColor = getTitleColorForState(m_eState); 代码如下:

const ccColor3B CCControlButton::getTitleColorForState(CCControlState state)
{
    ccColor3B returnColor = ccWHITE;
    do
    {
        CC_BREAK_IF(NULL == m_titleColorDispatchTable);
        CCColor3bObject* colorObject=(CCColor3bObject*)m_titleColorDispatchTable->objectForKey(state);    
        if (colorObject)
        {
            returnColor = colorObject->value;
            break;
        }

        colorObject = (CCColor3bObject*)m_titleColorDispatchTable->objectForKey(CCControlStateNormal);   
        if (colorObject)
        {
            returnColor = colorObject->value;
        }
    } while (0);

    return returnColor;
}

上面代码用于从字典m_titleColorDispatchTable中获得当前控件状态对应的CCColor3bObject对象,然后提取它的ccColor3B值,字典只能存放CCObject的子类,ccColor3B是结构,所以用CCColor3bObject包装了下ccColor3B。
m_titleColorDispatchTable->objectForKey(state)就是从字典中获得关键字对应的对象,然后如果colorObject存在就提取ccColor3B返回,不存在就调用m_titleColorDispatchTable->objectForKey(CCControlStateNormal)获得默认的值。这里默认值是在初始化完成的,在上面代码的后面,所以这里返回的是个ccWHITE对应白色。
this->setTitleLabel(getTitleLabelForState(m_eState));是设置当前状态标签,getTitleLabelForState(m_eState)代码如下:

CCNode* CCControlButton::getTitleLabelForState(CCControlState state)
{
    CCNode* titleLabel = (CCNode*)m_titleLabelDispatchTable->objectForKey(state);    
    if (titleLabel)
    {
        return titleLabel;
    }
    return (CCNode*)m_titleLabelDispatchTable->objectForKey(CCControlStateNormal);
}

上面也同样从标签表中获得对应状态标签,如果不存在就返回默认的。所以m_titleLabel为NULL,这里设置为空一点关系都没,因为初始化函数后面会显示的设置默认状态对应的标签、图片等,然后还会调用needLayout。
this->setBackgroundSprite(this->getBackgroundSpriteForState(m_eState));代码如下:

CCScale9Sprite* CCControlButton::getBackgroundSpriteForState(CCControlState state)
{
    CCScale9Sprite* backgroundSprite = (CCScale9Sprite*)m_backgroundSpriteDispatchTable->objectForKey(state);    
    if (backgroundSprite)
    {
        return backgroundSprite;
    }
    return (CCScale9Sprite*)m_backgroundSpriteDispatchTable->objectForKey(CCControlStateNormal);
}

上面代码也是获得当前状态背景图片,没有就为默认的,这里m_backgroundSprite为NULL。
m_doesAdjustBackgroundImage在初始化里面传的是true,所以会进行対背景进行调整。这里m_backgroundSprite为NULL并不进入里面,初始化后面的对needLayout的显示调用会进入。后面代码由于此时标签与背景对象都为NULL,所以没有执行。后面的needLayout再分析。
setAdjustBackgroundImage(true);分析完了,继续分析initWithLabelAndBackgroundSprite,此时initWithLabelAndBackgroundSprite继续调用setPreferredSize(CCSizeZero);代码如下:

void CCControlButton::setPreferredSize(CCSize size)
{
    if(size.width == 0 && size.height == 0)
    {
        m_doesAdjustBackgroundImage = true;
    }
    else
    {
        m_doesAdjustBackgroundImage = false;
        CCDictElement * item = NULL;
        CCDICT_FOREACH(m_backgroundSpriteDispatchTable, item)
        {
            CCScale9Sprite* sprite = (CCScale9Sprite*)item->getObject();
            sprite->setPreferredSize(size);
        }
    }

    m_preferredSize = size;
    needsLayout();
}

上面传入的参数时CCSizeZero,所以代码执行m_doesAdjustBackgroundImage = true;,然后又执行needsLayout();于是好多代码被重复调用,它的初始化调用了很多提供给客户的接口,客户调用这些接口会对控件布局进行调整,但是初始化只需要进行一次布局调整,所以完全可以重写一个初始化函数。这里注意下,现在m_preferredSize=CCSizeZero。然后通过ignoreAnchorPointForPosition(false);setAnchorPoint(ccp(0.5f, 0.5f));改变控件的锚点。
接下来setTitleLabel(node);setBackgroundSprite(backgroundSprite);这个时候才给m_titleLabel、m_backgroundSprite赋了值。
setColor(ccc3(255.0f, 255.0f, 255.0f));与setOpacity(255.0f);是设置所有状态对应的背景颜色与不透明度setColor(ccc3(255.0f, 255.0f, 255.0f))代码如下:

void CCControlButton::setColor(const ccColor3B & color)
{
    CCControl::setColor(color);

    CCDictElement * item = NULL;
    CCDICT_FOREACH(m_backgroundSpriteDispatchTable, item)
    {
        CCScale9Sprite* sprite = (CCScale9Sprite*)item->getObject();
        sprite->setColor(color);
    }
}

上面遍历背景图片表,设置每个精灵的顶点颜色为白色,用于着色器与纹理像素相乘得最终片元颜色。
setOpacity(255.0f)代码如下:

void CCControlButton::setOpacity(GLubyte opacity)
{
    // XXX fixed me if not correct
    CCControl::setOpacity(opacity);
    CCDictElement * item = NULL;
    CCDICT_FOREACH(m_backgroundSpriteDispatchTable, item)
    {
        CCScale9Sprite* sprite = (CCScale9Sprite*)item->getObject();
        sprite->setOpacity(opacity);
    }
}

遍历背景图片表,设置每个精灵的不透明度,用于与顶点颜色相乘,修改顶点颜色。
setOpacityModifyRGB(true);设置m_bOpacityModifyRGB,开启可以通过不透明度修改RGB的值,这里的修改不透明度不是直接改alpha值,改的是RGB的值,代码如下:

void CCSprite::setOpacity(GLubyte opacity)
{
    CCNodeRGBA::setOpacity(opacity);

    updateColor();
}

void CCSprite::updateColor(void)
{
    ccColor4B color4 = { _displayedColor.r, _displayedColor.g, _displayedColor.b, _displayedOpacity };

    // special opacity for premultiplied textures
    if (m_bOpacityModifyRGB)
    {
        color4.r *= _displayedOpacity/255.0f;
        color4.g *= _displayedOpacity/255.0f;
        color4.b *= _displayedOpacity/255.0f;
    }

    m_sQuad.bl.colors = color4;
    m_sQuad.br.colors = color4;
    m_sQuad.tl.colors = color4;
    m_sQuad.tr.colors = color4;

    // renders using batch node
    if (m_pobBatchNode)
    {
        if (m_uAtlasIndex != CCSpriteIndexNotInitialized)
        {
            m_pobTextureAtlas->updateQuad(&m_sQuad, m_uAtlasIndex);
        }
        else
        {
            // no need to set it recursively
            // update dirty_, don't update recursiveDirty_
            setDirty(true);
        }
    }

    // self render
    // do nothing
}

上面color4的rgb值都乘以了_displayedOpacity/255.0f,这个_displayedOpacity就是由CCNodeRGBA::setOpacity(opacity)修改的值。
继续分析控件初始化代码,下面回调下面几个函数:

CCString* tempString = CCString::create(label->getString());
//tempString->autorelease();
setTitleForState(tempString, CCControlStateNormal);
setTitleColorForState(rgbaLabel->getColor(), CCControlStateNormal);
setTitleLabelForState(node, CCControlStateNormal);
setBackgroundSpriteForState(backgroundSprite, CCControlStateNormal);

setLabelAnchorPoint(ccp(0.5f, 0.5f));

上面这个时候才设置了一开始创建的四个表对应的字典。设置了默认状态下的标签文本、标签颜色、标签以及背景图片。setBackgroundSpriteForState(backgroundSprite, CCControlStateNormal)代码如下:

void CCControlButton::setBackgroundSpriteForState(CCScale9Sprite* sprite, CCControlState state)
{
    CCSize oldPreferredSize = m_preferredSize; //当前为CCPOINTZERO

    CCScale9Sprite* previousBackgroundSprite = (CCScale9Sprite*)m_backgroundSpriteDispatchTable->objectForKey(state);获得之前state对应的背景图片
    if (previousBackgroundSprite)//之前这个state对应的背景图片存在,那么就要删除这个背景图片,字典中K/V也要删除
    {
        removeChild(previousBackgroundSprite, true);
        m_backgroundSpriteDispatchTable->removeObjectForKey(state);
    }

    m_backgroundSpriteDispatchTable->setObject(sprite, state);//添加到字典
    sprite->setVisible(false);//不可见
    sprite->setAnchorPoint(ccp(0.5f, 0.5f));//锚点设置
    addChild(sprite);//加进来

    if (this->m_preferredSize.width != 0 || this->m_preferredSize.height != 0) //背景大小不为0,初始化中,背景大小应该设置为0了,所以进不去
    {
        if (oldPreferredSize.equals(m_preferredSize))
        {
            // Force update of preferred size
            sprite->setPreferredSize(CCSizeMake(oldPreferredSize.width+1, oldPreferredSize.height+1));
        }

        sprite->setPreferredSize(this->m_preferredSize);
    }

    // If the current state if equal to the given state we update the layout
    if (getState() == state) //默认状态normal
    {
        needsLayout();
    }
}

上面初始化后getState获得的是默认状态,查看父类代码:

CCControl::CCControl()
: m_bIsOpacityModifyRGB(false)
, m_eState(CCControlStateNormal)
, m_hasVisibleParents(false)
, m_bEnabled(false)
, m_bSelected(false)
, m_bHighlighted(false)
, m_pDispatchTable(NULL)
{

}

上面m_eState一开始初始化为CCControlStateNormal了。所以needsLayout()或被调用,这个时候有调用needsLayout了,这个时候标签于背景都有了,needsLayout里代码会被执行。
接着setLabelAnchorPoint(ccp(0.5f, 0.5f));设置标签的锚点。初始化的最后needsLayout再次被调用。

上面所有的代码分析可以得出,CCControlButton它必然存在标签与背景图片,如果没有设置标签内容,标签大小为0罢了。CCControlButton有几个状态,可以通过改变它的状态对应的背景图片、标签、颜色、不透明度以及文本实现不同事件的不同显示效果,
下面是测试效果图:

当刚触摸、触摸后在里面拖动、触摸后从外面拖进是出现红色的标签,代码如戏:

btn->setTitleForState(CCString::create("物品"), CCControlStateHighlighted);
btn->setTitleColorForState(ccc3(255, 0, 0), CCControlStateHighlighted);

控件的状态有下面几种

状态 描述
CCControlStateNormal 没有触摸 The normal, or default state of a control°™that is, enabled but neither selected nor highlighted
CCControlStateHighlighted 触摸了 A control enters this state when a touch down, drag inside or drag enter is performed. You can retrieve and set this value through the highlighted property
CCControlStateDisabled 关闭未激活 disabled state of a control. This state indicates that the control is currently disabled. You can retrieve and set this value through the enabled property
CCControlStateSelected 选择激活 Selected state of a control. This state indicates that the control is currently selected. You can retrieve and set this value through the selected property

CCControlButton并没有上面所有的状态,下面分析下它如何添加目标动作以及调用目标动作的。

CCControlButton添加目标动作

btn->addTargetWithActionForControlEvents(this, cccontrol_selector(TilemapTest::itemsBarClick), CCControlEventTouchDown);添加了目标this,动作为this的itemsBarClick,对应事件CCControlEventTouchDown,当刚触摸时会调用this的itemsBarClick方法。addTargetWithActionForControlEvents代码如下:

void CCControl::addTargetWithActionForControlEvents(CCObject* target, SEL_CCControlHandler action, CCControlEvent controlEvents)
{
    // For each control events
    for (int i = 0; i < kControlEventTotalNumber; i++)
    {
        // If the given controlEvents bitmask contains the curent event
        if ((controlEvents & (1 << i)))
        {
            this->addTargetWithActionForControlEvent(target, action, 1<

上面代码遍历所有的事件,然后看controlEvents跟哪些事件按位与后为真,也就是controlEvents包含哪些事件。如果包含这个事件那么就执行this->addTargetWithActionForControlEvent(target, action, 1<

#define kControlEventTotalNumber 9
enum
{
    CCControlEventTouchDown           = 1 << 0,    // A touch-down event in the control.
    CCControlEventTouchDragInside     = 1 << 1,    // An event where a finger is dragged inside the bounds of the control.
    CCControlEventTouchDragOutside    = 1 << 2,    // An event where a finger is dragged just outside the bounds of the control.
    CCControlEventTouchDragEnter      = 1 << 3,    // An event where a finger is dragged into the bounds of the control.
    CCControlEventTouchDragExit       = 1 << 4,    // An event where a finger is dragged from within a control to outside its bounds.
    CCControlEventTouchUpInside       = 1 << 5,    // A touch-up event in the control where the finger is inside the bounds of the control.
    CCControlEventTouchUpOutside      = 1 << 6,    // A touch-up event in the control where the finger is outside the bounds of the control.
    CCControlEventTouchCancel         = 1 << 7,    // A system event canceling the current touches for the control.
    CCControlEventValueChanged        = 1 << 8      // A touch dragging or otherwise manipulating a control, causing it to emit a series of different values.
};
typedef unsigned int CCControlEvent;

上面没把CCControlEvent直接定义为枚举类型,所以函数的参数可以直接传整数作为参数,不清楚作者这样的目的。kControlEventTotalNumber必须在外面定义,因为枚举里面不是从0开始的,不然放在最后面就可以表示它前面元素个数了。
this->addTargetWithActionForControlEvent(target, action, 1<

void CCControl::addTargetWithActionForControlEvent(CCObject* target, SEL_CCControlHandler action, CCControlEvent controlEvent)
{    
    // Create the invocation object
    CCInvocation *invocation = CCInvocation::create(target, action, controlEvent); //生成一个动作包装

    // Add the invocation into the dispatch list for the given control event
    CCArray* eventInvocationList = this->dispatchListforControlEvent(controlEvent);
    eventInvocationList->addObject(invocation);    //把动作包装加入到事件分发列表中
}

上面CCInvocation::create(target, action, controlEvent)代码如下:

CCInvocation* CCInvocation::create(CCObject* target, SEL_CCControlHandler action, CCControlEvent controlEvent)
{
    CCInvocation* pRet = new CCInvocation(target, action, controlEvent);
    if (pRet != NULL)
    {
        pRet->autorelease();
    }
    return pRet;
}

CCInvocation::CCInvocation(CCObject* target, SEL_CCControlHandler action, CCControlEvent controlEvent)
: m_actionMethod(NativeAction)
, m_functionId(0)
{
    m_target=target;
    m_action=action;
    m_controlEvent=controlEvent;
}

仅仅是利用CCInvocation对目标、动作、事件进行了一个包装,让代码更干净点。注意m_actionMethod(NativeAction),它表示是C++代码调用,不是lua。

this->dispatchListforControlEvent(controlEvent)代码如下:

CCArray* CCControl::dispatchListforControlEvent(CCControlEvent controlEvent)
{
    CCArray* invocationList = static_cast(m_pDispatchTable->objectForKey((int)controlEvent));

    // If the invocation list does not exist for the  dispatch table, we create it
    if (invocationList == NULL)
    {
        invocationList = CCArray::createWithCapacity(1);
        m_pDispatchTable->setObject(invocationList, controlEvent);
    }    
    return invocationList;
}

CCDictionary* m_pDispatchTable是父类CCControl定义的,它是事件分发表,保存了每个事件对应的调用列表(保存CCInvocation的数组)。上面代码先从获得m_pDispatchTable中获得对应的调用列表,如果没有就建一个数组然后把(controlEvent, invocationList)这个键值对加入字典,最后返回调用列表。
eventInvocationList->addObject(invocation)向返回的调用列表添加一个调用。
上面代码就是把用户定义的一个目标动作与事件包装成一个调用CCInvocation,然后加入到对应的事件调用队列中去。下面分析下这些调用是如何被触发的。

CCControlButton调用目标动作

下面分析CCControlButton的触摸事件处理
ccTouchBegan代码如下:

bool CCControlButton::ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent)
{
    if (!isTouchInside(pTouch) || !isEnabled() || !isVisible() || !hasVisibleParents() )
    {
        return false;
    }

    for (CCNode *c = this->m_pParent; c != NULL; c = c->getParent())
    {
        if (c->isVisible() == false)
        {
            return false;
        }
    }

    m_isPushed = true;
    this->setHighlighted(true);
    sendActionsForControlEvents(CCControlEventTouchDown);
    return true;
}

对于触摸没命中、没开启触摸、不可见或者父亲不可见的情况就不处理。这里的for(CCNode *c = this->m_pParent; c != NULL; c = c->getParent())是递归的检查父亲是否可见,只要CCControlButton到根路径上有一个父亲不可见就不处理。
this->setHighlighted(true)设置控件高亮状态,代码如下:

void CCControlButton::setHighlighted(bool enabled)
{
    if (enabled == true)
    {
        m_eState = CCControlStateHighlighted; //改变为CCControlStateHighlighted状态
    }
    else
    {
        m_eState = CCControlStateNormal; //改变为CCControlStateNormal状态
    }

    CCControl::setHighlighted(enabled); //开启高亮,并会重新布局

    CCAction *action = getActionByTag(kZoomActionTag);
    if (action)
    {
        stopAction(action);      //停止放大缩小动作
    }
    needsLayout();
    if( m_zoomOnTouchDown )
    {
        float scaleValue = (isHighlighted() && isEnabled() && !isSelected()) ? 1.1f : 1.0f;
        CCAction *zoomAction = CCScaleTo::create(0.05f, scaleValue);
        zoomAction->setTag(kZoomActionTag);
        runAction(zoomAction); //执行放大缩小动作
    }
}

void CCControl::setHighlighted(bool bHighlighted)
{
    m_bHighlighted = bHighlighted;
    this->needsLayout();
}

上面代码主要记录了控件状态,开启触摸时状态为CCControlStateHighlighted,后面有用。m_zoomOnTouchDown为真的情况下,控件会放大到1.1,后面isHighlighted()为false即CCControlStateNormal状态,又会缩小到1.0。最后的sendActionsForControlEvents(CCControlEventTouchDown);就是执行之前用户注册的目标事件,代码如下:

void CCControl::sendActionsForControlEvents(CCControlEvent controlEvents)
{
    // For each control events
    for (int i = 0; i < kControlEventTotalNumber; i++)
    {
        // If the given controlEvents bitmask contains the curent event
        if ((controlEvents & (1 << i)))
        {
            // Call invocations
            // 
            CCArray* invocationList = this->dispatchListforControlEvent(1<<i);
            CCObject* pObj = NULL;
            CCARRAY_FOREACH(invocationList, pObj)
            {
                CCInvocation* invocation = (CCInvocation*)pObj;
                invocation->invoke(this);
            }
            //Call ScriptFunc
            if (kScriptTypeNone != m_eScriptType)
            {
                int nHandler = this->getHandleOfControlEvent(controlEvents);
                if (-1 != nHandler) {
                    CCArray* pArrayArgs = CCArray::createWithCapacity(3);
                    pArrayArgs->addObject(CCString::create(""));
                    pArrayArgs->addObject(this);
                    pArrayArgs->addObject(CCInteger::create(1 << i));
                    CCScriptEngineManager::sharedManager()->getScriptEngine()->executeEventWithArgs(nHandler, pArrayArgs);
                }
            }
        }
    }
}

上面代码跟之前添加到事件对于的调度列表中一样,现在是遍历事件列表,然后调用命中事件列表中的所有调用CCInvocation。invocation->invoke(this)代码如下:

void CCInvocation::invoke(CCObject* sender)
{
    if (m_actionMethod == NativeAction) {
        if (m_target && m_action)
        {
            (m_target->*m_action)(sender, m_controlEvent);
        }
    }
    else {
        CCPoolManager::sharedPoolManager()->push();
        CCInteger* num = CCInteger::create(m_controlEvent);
        CCArray* args = CCArray::create(num, nullptr);
        CCLuaEngine::defaultEngine()->executeEventWithArgs(m_functionId, args);
        CCPoolManager::sharedPoolManager()->pop();
    }
}

m_actionMethod == NativeAction,此时(m_target->*m_action)(sender, m_controlEvent)被执行,目标m_target的函数*m_action被调用,并传入sender、m_controlEvent,这也是为什么用户定义的函数必须这两个类型参数原因了。

ccTouchMoved代码如下:

void CCControlButton::ccTouchMoved(CCTouch *pTouch, CCEvent *pEvent)
{    
    if (!isEnabled() || !isPushed() || isSelected()) //没开启、暂停、没选择就不处理
    {
        if (isHighlighted())
        {
            setHighlighted(false);
        }
        return;
    }

    bool isTouchMoveInside = isTouchInside(pTouch);
    if (isTouchMoveInside && !isHighlighted())
    {
        setHighlighted(true);
        sendActionsForControlEvents(CCControlEventTouchDragEnter);
    }
    else if (isTouchMoveInside && isHighlighted())
    {
        sendActionsForControlEvents(CCControlEventTouchDragInside);
    }
    else if (!isTouchMoveInside && isHighlighted())
    {
        setHighlighted(false);

        sendActionsForControlEvents(CCControlEventTouchDragExit);        
    }
    else if (!isTouchMoveInside && !isHighlighted())
    {
        sendActionsForControlEvents(CCControlEventTouchDragOutside);        
    }
}

开始触摸下去时控件处于高亮状态,现在ccTouchMoved的的状态与触发事件如下(-表示状态不变):

之前状态 控件内部 触发的事件 改变后状态
CCControlStateNormal yes CCControlEventTouchDragEnter CCControlStateHighlighted
CCControlStateHighlighted yes CCControlEventTouchDragInside -
CCControlStateHighlighted no CCControlEventTouchDragExit CCControlStateNormal
CCControlStateNormal no CCControlEventTouchDragOutside -

上面CCControlStateNormal与CCControlStateHighlighted之间切换,CCControlButton就这两种状态。

ccTouchEnded代码如下:

void CCControlButton::ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent)
{
    m_isPushed = false;
    setHighlighted(false);

    if (isTouchInside(pTouch))
    {
        sendActionsForControlEvents(CCControlEventTouchUpInside);        
    }
    else
    {
        sendActionsForControlEvents(CCControlEventTouchUpOutside);        
    }
}

当放开手时,状态恢复为CCControlStateNormal,isTouchInside(pTouch)为true,即在控件里面松手时,触发CCControlEventTouchUpInside事件,在外面触发CCControlEventTouchUpOutside事件。

CCControlButton分析总结

上面对CCControlButton做了比较细的代码分析,研究了它是如何创建的,它的内部由标签与图片构成,它有两种状态,可以通过设置不同状态对应的图片、文本、颜色等使它在不同状态表现不同。研究了它是如何添加不同事件对应的目标动作以及这些目标动作怎么被调用的。它有七个事件,两个状态,一开始处于CCControlStateNormal状态,后面通过用户的三种触摸事件以及触摸区域触发了不同的事件,并对一些状态进行改变。我们可以定制自己的控件,下面是一些定制要考虑的东西:

  1. 控件做什么,按钮、滑动条还是滚动条
  2. 控件的部件构成,有多少个文字标签,多少个图片,多少个其它子控件
  3. 控件有多少个状态(由条件与之前状态生成新的状态),用状态转换图描述
  4. 控件有多少的事件(由条件与之前状态触发),可以画事件转换图
  5. 控件有多少外部条件(由触摸、命中、时间变化等触发) 列出问题需要的条件

你可能感兴趣的:(游戏引擎,游戏)