首页 > 代码库 > cocos2d-x 源码分析 : control 源码分析 ( 控制类组件 controlButton)

cocos2d-x 源码分析 : control 源码分析 ( 控制类组件 controlButton)

源码版本来自3.1rc

转载请注明


cocos2d-x源码分析总目录

http://blog.csdn.net/u011225840/article/details/31743129


1.继承结构

       control的设计整体感觉挺美的,在父类control定义了整个控制事件的基础以及管理,虽然其继承了Layer,但其本身和UI组件的实现并没有关联。在子类(controlButton,controlSwitch,controlStepper等中实现不同的UI组件)。下面通过源码来分析control与controlButton,一起来体会下面向对象的魅力。

2.源码分析

        2.1control 

          看过我Scheduler源码分析的朋友应该熟悉,Scheduler本身属于一种Manager,而具体定时的动作来自于CallBackSelector。在control整体的设计中也是如此,control中定义了一系列情况,来订制合适触发何种事件,而该事件是否触发某种Invocation,是可以动态设置的。该Invocation就可以理解为具体的动作。

2.1.1 EventType

    enum class EventType
    {
        TOUCH_DOWN           = 1 << 0,    // A touch-down event in the control.
        DRAG_INSIDE          = 1 << 1,    // An event where a finger is dragged inside the bounds of the control.
        DRAG_OUTSIDE         = 1 << 2,    // An event where a finger is dragged just outside the bounds of the control.
        DRAG_ENTER           = 1 << 3,    // An event where a finger is dragged into the bounds of the control.
        DRAG_EXIT            = 1 << 4,    // An event where a finger is dragged from within a control to outside its bounds.
        TOUCH_UP_INSIDE      = 1 << 5,    // A touch-up event in the control where the finger is inside the bounds of the control.
        TOUCH_UP_OUTSIDE     = 1 << 6,    // A touch-up event in the control where the finger is outside the bounds of the control.
        TOUCH_CANCEL         = 1 << 7,    // A system event canceling the current touches for the control.
        VALUE_CHANGED        = 1 << 8      // A touch dragging or otherwise manipulating a control, causing it to emit a series of different values.
    };

        开始时,看见如此定义其实有些不懂。但是为何需要这么设置呢,这样可以通过| 操作同时指定两个Event事件,而如果简单的使用 1 2 3 4,就不能通过|或者其他操作来唯一确定多个事件。
        从上到下,事件分别是在内部触摸,内部拖动,外部拖动,拖动时进入,拖动时离开,内部松开,外部松开,取消,值发生改变。

2.1.2 State

       
    enum class State
    {
        NORMAL         = 1 << 0, // The normal, or default state of a control—that is, enabled but neither selected nor highlighted.
        HIGH_LIGHTED   = 1 << 1, // Highlighted state of a control. 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.
        DISABLED       = 1 << 2, // 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.
        SELECTED       = 1 << 3  // 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.
    };

       在control组件下,每一个state都会有对应的UI形态,普通状态下,UI展示的view可以同被选择状态下UI展示的view不同。通过一个map来对应state和UI的存取。

2.1.3 Control Events 的管理

       2.1.3.1 sendActionsForControlEvents

       触发对应事件列表的事件action,注意Events是如何表示的(通过bit位的相符,而不是一个list,速度快!)
void Control::sendActionsForControlEvents(EventType controlEvents)
{
	//retain和release的作用是保证执行该actions的过程中,control不会被delete。
	//可能会有actions会release 事件来源Ref--control,所以需要先retain,保证其执行完所有events后再release。
	retain();
    // For each control events
    for (int i = 0; i < kControlEventTotalNumber; i++)
    {
        // If the given controlEvents bitmask contains the curent event
		//bit位适配
        if (((int)controlEvents & (1 << i)))
        {
            // Call invocations
            const auto& invocationList = this->dispatchListforControlEvent((Control::EventType)(1<<i));

            for(const auto &invocation : invocationList) {
                invocation->invoke(this);
            }
        
        }
    }
	release();
}

Vector<Invocation*>& Control::dispatchListforControlEvent(EventType controlEvent)
{
	//这个函数的作用是获得该类事件类型的InvocationVector
    Vector<Invocation*>* invocationList = nullptr;
    auto iter = _dispatchTable.find((int)controlEvent);
    
    // If the invocation list does not exist for the  dispatch table, we create it
    if (iter == _dispatchTable.end())
    {
        invocationList = new Vector<Invocation*>();
        _dispatchTable[(int)controlEvent] = invocationList;
    }
    else
    {
        invocationList = iter->second;
    }
    return *invocationList;
}


     2.1.3.2 addTargetWithActionForControlEvents

     
void Control::addTargetWithActionForControlEvents(Ref* target, Handler action, EventType controlEvents)
{
    // For each control events
    for (int i = 0; i < kControlEventTotalNumber; i++)
    {
        // If the given controlEvents bitmask contains the curent event
        if (((int)controlEvents & (1 << i)))
        {
            this->addTargetWithActionForControlEvent(target, action, (EventType)(1<<i));
        }
    }
}
void Control::addTargetWithActionForControlEvent(Ref* target, Handler action, EventType controlEvent)
{    
    // Create the invocation object
    Invocation *invocation = Invocation::create(target, action, controlEvent);

    // Add the invocation into the dispatch list for the given control event
    auto& eventInvocationList = this->dispatchListforControlEvent(controlEvent);
	//此时pushback的同时也会retain
    eventInvocationList.pushBack(invocation);
}

2.1.3.3 removeTargetWithActionForControlEvent

void Control::removeTargetWithActionForControlEvents(Ref* target, Handler action, EventType controlEvents)
{
     // For each control events
    for (int i = 0; i < kControlEventTotalNumber; i++)
    {
        // If the given controlEvents bitmask contains the curent event
        if (((int)controlEvents & (1 << i)))
        {
            this->removeTargetWithActionForControlEvent(target, action, (EventType)(1 << i));
        }
    }
}


 
void Control::removeTargetWithActionForControlEvent(Ref* target, Handler action, EventType controlEvent)
{
    // Retrieve all invocations for the given control event
    //<Invocation*>
    auto& eventInvocationList = this->dispatchListforControlEvent(controlEvent);
    
    //remove all invocations if the target and action are null
    //TODO: should the invocations be deleted, or just removed from the array? Won't that cause issues if you add a single invocation for multiple events?

    if (!target && !action)
    {
        //remove objects
        eventInvocationList.clear();
    } 
    else
    {
        std::vector<Invocation*> tobeRemovedInvocations;
        
        //normally we would use a predicate, but this won't work here. Have to do it manually
        for(const auto &invocation : eventInvocationList) {
            bool shouldBeRemoved=true;
            if (target)
            {
                shouldBeRemoved=(target==invocation->getTarget());
            }
            if (action)
            {
                shouldBeRemoved=(shouldBeRemoved && (action==invocation->getAction()));
            }
            // Remove the corresponding invocation object
            if (shouldBeRemoved)
            {
                tobeRemovedInvocations.push_back(invocation);
            }
        }

        for(const auto &invocation : tobeRemovedInvocations) {
            eventInvocationList.eraseObject(invocation);
        }
    }
}


       在介绍controlbutton之前,我必须要再次强调下control源码关于事件类型和状态的处理:使用bit位是否match可以唯一确定存,并可以消除使用list的影响,适合于enum类比较少且需要同时传递多个的情况。

2.2 ControlButton

      2.2.1 Touch

        对于controlButton,何时触发什么事件是在触摸机制中决定的,通过分析其源码可以很好看出。

       
bool ControlButton::onTouchBegan(Touch *pTouch, Event *pEvent)
{
	//是否接收该touch
    if (!isTouchInside(pTouch) || !isEnabled() || !isVisible() || !hasVisibleParents() )
    {
        return false;
    }
    
	//感觉这段与hasVisibleParents重复了,可以删除
    for (Node *c = this->_parent; c != nullptr; c = c->getParent())
    {
        if (c->isVisible() == false)
        {
            return false;
        }
    }
    
    _isPushed = true;
    this->setHighlighted(true);
	//touch down事件只在began中触发
    sendActionsForControlEvents(Control::EventType::TOUCH_DOWN);
    return true;
}


void ControlButton::onTouchMoved(Touch *pTouch, Event *pEvent)
{ 
	
    if (!isEnabled() || !isPushed() || isSelected())
    {
        if (isHighlighted())
        {
            setHighlighted(false);
        }
        return;
    }
   
    bool isTouchMoveInside = isTouchInside(pTouch);
	//在inside内部move并且当前状态不是highlight,说明从外部移入到内部,触发事件drag enter
    if (isTouchMoveInside && !isHighlighted())
    {
        setHighlighted(true);
        sendActionsForControlEvents(Control::EventType::DRAG_ENTER);
    }
	//inside内部move并且当前状态时highlight,说明在内部移动,触发事件 drag inside
    else if (isTouchMoveInside && isHighlighted())
    {
        sendActionsForControlEvents(Control::EventType::DRAG_INSIDE);
    }
	//outside move 但是当前状态是highlight,证明从内移动到外,触发事件drag exit
    else if (!isTouchMoveInside && isHighlighted())
    {
        setHighlighted(false);
        
        sendActionsForControlEvents(Control::EventType::DRAG_EXIT);        
    }
	//outside move 并且 不是highlight 在外部移动,触发事件 drag outside
    else if (!isTouchMoveInside && !isHighlighted())
    {
        sendActionsForControlEvents(Control::EventType::DRAG_OUTSIDE);        
    }
}

void ControlButton::onTouchEnded(Touch *pTouch, Event *pEvent)
{
    _isPushed = false;
    setHighlighted(false);
    
    //在这里其实应该增加判断的,对于controlButton放在scrollView或者tableView或者可以移动的layer上的时候
	//应该给用户一个开关选择,根据移动了距离的多少判断用户是否要触发touch up inside和 outside 事件。
    if (isTouchInside(pTouch))
    {
        sendActionsForControlEvents(Control::EventType::TOUCH_UP_INSIDE);        
    }
    else
    {
        sendActionsForControlEvents(Control::EventType::TOUCH_UP_OUTSIDE);        
    }
}

void ControlButton::onTouchCancelled(Touch *pTouch, Event *pEvent)
{
    _isPushed = false;
    setHighlighted(false);
    sendActionsForControlEvents(Control::EventType::TOUCH_CANCEL);
}

2.2.2 create and needlayout

      control button 本质是一个label与一个scale9sprite,在其初始化中可以看出。

      2.2.2.1 create相关

      
bool ControlButton::initWithLabelAndBackgroundSprite(Node* node, Scale9Sprite* backgroundSprite)
{
    if (Control::init())
    {
        CCASSERT(node != nullptr, "Label must not be nil.");
        LabelProtocol* label = dynamic_cast<LabelProtocol*>(node);
        CCASSERT(backgroundSprite != nullptr, "Background sprite must not be nil.");
        CCASSERT(label != nullptr || backgroundSprite != nullptr, "");
        
        _parentInited = true;

        _isPushed = false;

        // Adjust the background image by default
        setAdjustBackgroundImage(true);
        setPreferredSize(Size::ZERO);
        // Zooming button by default
        _zoomOnTouchDown = true;
        _scaleRatio = 1.1f;
        
        // Set the default anchor point
        ignoreAnchorPointForPosition(false);
        setAnchorPoint(Vec2::ANCHOR_MIDDLE);
        
        // Set the nodes,label
        setTitleLabel(node);
        setBackgroundSprite(backgroundSprite);

        // Set the default color and opacity
        setColor(Color3B::WHITE);
        setOpacity(255.0f);
        setOpacityModifyRGB(true);
        
        // Initialize the dispatch table,开始时候的状态皆为normal
        
        setTitleForState(label->getString(), Control::State::NORMAL);
        setTitleColorForState(node->getColor(), Control::State::NORMAL);
        setTitleLabelForState(node, Control::State::NORMAL);
        setBackgroundSpriteForState(backgroundSprite, Control::State::NORMAL);
        
        setLabelAnchorPoint(Vec2::ANCHOR_MIDDLE);

        // Layout update
        needsLayout();

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

          controlButton通过4个map,将状态相关的信息与UI需要显示的view存储起来。titleDispatch存放的是不同状态下label的string,titleColor存放的是不同状态下label的颜色,titleLabel存放的是不同状态下title绑定的Node,backgroundsprite是不同状态下的sprite。     

    std::unordered_map<int, std::string> _titleDispatchTable;
    std::unordered_map<int, Color3B> _titleColorDispatchTable;

    Map<int, Node*> _titleLabelDispatchTable;
    Map<int, Scale9Sprite*> _backgroundSpriteDispatchTable;

        并且通过一系列get set函数将状态与这些属性相关联,具体的不再赘述。


2.2.2.2 needlayout

      
void ControlButton::needsLayout()
{
	//整体步骤:获取特定状态下的label和sprite,然后将button的size设置为两者的最大值,然后显示两者
    if (!_parentInited) {
        return;
    }
    // Hide the background and the label
    if (_titleLabel != nullptr) {
        _titleLabel->setVisible(false);
    }
    if (_backgroundSprite) {
        _backgroundSprite->setVisible(false);
    }
    // Update anchor of all labels
    this->setLabelAnchorPoint(this->_labelAnchorPoint);
    
    // Update the label to match with the current state
    _currentTitle = getTitleForState(_state);

    _currentTitleColor = getTitleColorForState(_state);

    this->setTitleLabel(getTitleLabelForState(_state));

    LabelProtocol* label = dynamic_cast<LabelProtocol*>(_titleLabel);
    if (label && !_currentTitle.empty())
    {
        label->setString(_currentTitle);
    }

    if (_titleLabel)
    {
        _titleLabel->setColor(_currentTitleColor);
    }
    if (_titleLabel != nullptr)
    {
        _titleLabel->setPosition(Vec2 (getContentSize().width / 2, getContentSize().height / 2));
    }
    
    // Update the background sprite
    this->setBackgroundSprite(this->getBackgroundSpriteForState(_state));
    if (_backgroundSprite != nullptr)
    {
        _backgroundSprite->setPosition(Vec2 (getContentSize().width / 2, getContentSize().height / 2));
    }
   
    // Get the title label size
    Size titleLabelSize;
    if (_titleLabel != nullptr)
    {
        titleLabelSize = _titleLabel->getBoundingBox().size;
    }
    
    // Adjust the background image if necessary
    if (_doesAdjustBackgroundImage)
    {
        // Add the margins
        if (_backgroundSprite != nullptr)
        {
            _backgroundSprite->setContentSize(Size(titleLabelSize.width + _marginH * 2, titleLabelSize.height + _marginV * 2));
        }
    } 
    else
    {        
        //TODO: should this also have margins if one of the preferred sizes is relaxed?
        if (_backgroundSprite != nullptr)
        {
            Size preferredSize = _backgroundSprite->getPreferredSize();
            if (preferredSize.width <= 0)
            {
                preferredSize.width = titleLabelSize.width;
            }
            if (preferredSize.height <= 0)
            {
                preferredSize.height = titleLabelSize.height;
            }

            _backgroundSprite->setContentSize(preferredSize);
        }
    }
    
    // Set the content size
	//总体来说,需要注意的就是这里,将两者size的最大值赋给本身
    Rect rectTitle;
    if (_titleLabel != nullptr)
    {
        rectTitle = _titleLabel->getBoundingBox();
    }
    Rect rectBackground;
    if (_backgroundSprite != nullptr)
    {
        rectBackground = _backgroundSprite->getBoundingBox();
    }

    Rect maxRect = ControlUtils::RectUnion(rectTitle, rectBackground);
    setContentSize(Size(maxRect.size.width, maxRect.size.height));        
    
    if (_titleLabel != nullptr)
    {
        _titleLabel->setPosition(Vec2(getContentSize().width/2, getContentSize().height/2));
        // Make visible the background and the label
        _titleLabel->setVisible(true);
    }
  
    if (_backgroundSprite != nullptr)
    {
        _backgroundSprite->setPosition(Vec2(getContentSize().width/2, getContentSize().height/2));
        _backgroundSprite->setVisible(true);   
    }   
}

3.小结

         关于其他control类组件包括:
         controlColourPicker:颜色选择器
         controlHuePicker:色调选择器
         controlSwitch:开关
         controlSlider:滑块
         controlStepper:计步器
         controlPotentioMeter:恒电位仪表。。。(一个圆形仪表,可以旋转,并且有一个圆形的progress bar)
         controlsaturationbrightnessPicker:饱和度亮度选择器