在开发一些窗口层次比复杂的cocos2d项目时,会发现一些由于没有窗口层次而引起的bug。这些bug让一些从windows平台过来的人觉得很无奈。比如,我们有一个列表控件,我们在其中放了一些菜单,当我们滑动列表控件使菜单选项(称为A)滑出列表控件的可视范围时,按理我们是无法再点击到A的,因为它滚动出了父控件可视范围,不被用户所看到。但是cocos2d的默认情况是能点击到的,这是因为cocos2d的消息管理是优先级消息机制,只要控件登记接收消息,那么cocos2d会一如既往的发给他。所以我们刚才讲的情形在cocos2d看来,它无法根据A被遮挡而不给A发消息。究其根本,是没有一个层级窗口消息机制(当然你能通过其他的方法帮助cocos2d,但我个人觉得有点不够彻底)。
我想建立一个相对完整cocos2d的的层级窗口消息机制,且不改变cocos2d任何源码(因为改变源码的话,不知道以后升级起来是不是很麻烦)。基本思路有如下几条:
在任何一个场景中,我们会有一个最底层的Layer(我称为祖层),这个Layer将接受cocos2d的触摸消息。并且这个Layer能将触摸消息传递给其所有的子控件。
一个场景中除了祖层之外,所有其他的控件都将不接受任何触摸消息,其触摸消息的来源于父控件。当然任何非叶子控件都会将其触摸消息传递给其子控件。
控件将消息传递给子控件时,如果子控件消息处理函数返回真,则记录该子控件,以方便将后续消息传递给这个子控件。
尽可能兼容已知和未知的cocos2d控件库,这样在这个机制类我们会有更多好用的子控件。
提供模式和非模式两种窗口模式。就像我们在用windows时,点新建文件,会弹出一个模式窗口,不关闭它,我们无法和其他窗口交互。
wmTouchDelegate并非继承于ccTouchDelegate或其他类(但消息处理函数名同ccTouchDelegate一样)。它是一个消息传播大使,所有继承于该类的类都能自动地将消息传播到所有的子控件。不继承于ccTouchDelegate主要出于设计原则中避免多重继承中的基类重复。
wmLayer继承于wmTouchDelegate和CCLayer(图中未指出),负责将消息处理函数转接到wmTouchDelegate的消息处理函数,这是因为wmTouchDelegate的消息处理函数同ccTouchDelegate是一样的,而CCLayer已经继承了ccTouchDelegate,这样如果不显示的转接处理函数,C++编译器会提示有两个版本选择的错误。
wmLayerAncestor继承于wmLayer,这样它已经具备了消息转发的功能。同时它接受触摸消息。并注册了优先级为0且吞并消息的touch target。这样它将是上面提到的祖层,一个Scene中只有一个祖层。
wmLayerDescendant继承于wmLayer,这样它已经具备了消息转发功能,同时它屏蔽了消息触摸消息。一个scene中多数层都应该是继承与wmLayerDescendant的。
wmLayerModal继承于wmLayerAncestor,所以它接受触摸消息,为了达到模式窗口的功能(程序中当有模式窗口时,只有最顶层的模式窗口能接受到消息,其他层都屏蔽),我们将wmLayerModal注册为优先级-128且吞并消息的窗口。
wmTouchDelegate:
wmTouchDelegate.h
// // Created by jason on 12-12-25. // // #ifndef __TableTest__wmTouchDelegate__ #define __TableTest__wmTouchDelegate__ #include <iostream> #include "cocos2d.h" USING_NS_CC; #define WM_TOUCH_DELEGATE_OWNER_CONSTRUCTOR_IN_HEAD_FILE( ownerClassName ) \ ownerClassName() : wmTouchDelegate( this ){} #define WM_TOUCH_DELEGATE_IMPLEMENT_IN_HEAD_FILE() \ virtual bool ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent) \ { \ return wmTouchDelegate::ccTouchBegan( pTouch, pEvent ); \ } \ virtual void ccTouchMoved(CCTouch *pTouch, CCEvent *pEvent) \ { \ wmTouchDelegate::ccTouchMoved( pTouch, pEvent ); \ } \ virtual void ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent) \ { \ wmTouchDelegate::ccTouchEnded( pTouch, pEvent ); \ } \ virtual void ccTouchCancelled(CCTouch *pTouch, CCEvent *pEvent) \ { \ wmTouchDelegate::ccTouchCancelled( pTouch, pEvent ); \ } //window message mechanism //We only change the TargetedTouch message mechansim. //Node who want standard touch message should register all by itself. class wmTouchDelegate { public: wmTouchDelegate( CCNode* pOwner ) : m_pOwner( pOwner ), m_bDraging( false ) { m_pItemsClaimTouch = CCArray::createWithCapacity( CHILD_MAX ); assert( m_pItemsClaimTouch ); m_pItemsClaimTouch->retain(); } virtual ~wmTouchDelegate() { CC_SAFE_RELEASE_NULL( m_pItemsClaimTouch ); } protected: // default implements are used to call script callback if exist virtual bool ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent); virtual void ccTouchMoved(CCTouch *pTouch, CCEvent *pEvent); virtual void ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent); virtual void ccTouchCancelled(CCTouch *pTouch, CCEvent *pEvent); private: void passMessage( CCNode* pParent, CCTouch *pTouch, CCEvent *pEvent ); private: CCNode* m_pOwner; bool m_bDraging; //items claim touch message CCArray* m_pItemsClaimTouch;
//store all class whose touch handler is non-trival static const void* m_pNonTrivalTouchHandlerClasses[];};#endif /* defined(__TableTest__wmTouchDelegate__) */
// // wmTouchDelegate.cpp // TableTest // // Created by jason on 12-12-25. // // #include "wmTouchDelegate.h" #include "cocos-ext.h" #include "wmControl.h"//include wmTableView,wmMenu,wmControl USING_NS_CC_EXT; const void* wmTouchDelegate::m_pNonTrivalTouchHandlerClasses[ ] = { &typeid( wmTableView ), &typeid( wmMenu ), &typeid( wmControlButton ), }; #pragma mark- input touche bool wmTouchDelegate::ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent) { if( !m_pOwner || !m_pOwner->isVisible() ) { return false; } CCPoint pt = m_pOwner->convertTouchToNodeSpace( pTouch ); CCRect rcBoundingBox( 0, 0, m_pOwner->getContentSize().width, m_pOwner->getContentSize().height ); //whether hit the node if( !rcBoundingBox.containsPoint( pt ) ) { return false; } //pass message to all children passMessage( m_pOwner, pTouch, pEvent ); //if there is a child who claims for touch message, then self claims to return m_pItemsClaimTouch->count() > 0 ? true : false; } void wmTouchDelegate::ccTouchMoved(CCTouch *pTouch, CCEvent *pEvent) { //special process for menu, we won't pass ccTouchMoved message to menu. Because we think menu doesn't need ccTouchMoved message in ios device where user always want to dray layer instead menu. The fllowing block for menu will only go once. if( false == m_bDraging ) { for( int i = 0; i < m_pItemsClaimTouch->count(); ) { CCLayer* pItem = ( CCLayer* )m_pItemsClaimTouch->objectAtIndex( i ); //menu items doesn't process ccTouchMove(), cancel it. assert( NULL != pItem ); //if it's menu if( dynamic_cast< CCMenu* >( pItem ) ) { pItem->ccTouchCancelled( pTouch, pEvent ); m_pItemsClaimTouch->removeObjectAtIndex( i ); } else { ++i; } } } //pass ccTouchMoved message to un-CCMenu item int iNumItemsNotMenu = m_pItemsClaimTouch->count(); for( int i = 0; i < iNumItemsNotMenu; ++i ) { wmTouchDelegate* pItemWindowMessage = NULL; CCLayer* pItem = NULL; pItem = ( CCLayer* )m_pItemsClaimTouch->objectAtIndex( i ); assert( NULL != pItem ); //window message items if( ( pItemWindowMessage = dynamic_cast< wmTouchDelegate* >( pItem ) ) ) { pItemWindowMessage->ccTouchMoved( pTouch, pEvent ); } else//coscos2d-x items { pItem->ccTouchMoved( pTouch, pEvent ); } } m_bDraging = true; } void wmTouchDelegate::ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent) { int iNumItems = m_pItemsClaimTouch->count(); for( int i = 0; i < iNumItems; ++i ) { wmTouchDelegate* pItemWindowMessage = NULL; CCLayer* pItem = NULL; pItem = ( CCLayer* )m_pItemsClaimTouch->objectAtIndex( i ); assert( NULL != pItem ); //window message items if( ( pItemWindowMessage = dynamic_cast< wmTouchDelegate* >( pItem ) ) ) { pItemWindowMessage->ccTouchEnded( pTouch, pEvent ); } else//coscos2d-x items { pItem->ccTouchEnded( pTouch, pEvent ); } } m_pItemsClaimTouch->removeAllObjects(); m_bDraging = false; } void wmTouchDelegate::ccTouchCancelled(CCTouch *pTouch, CCEvent *pEvent) { int iNumItems = m_pItemsClaimTouch->count(); for( int i = 0; i < iNumItems; ++i ) { wmTouchDelegate* pItemWindowMessage = NULL; CCLayer* pItem = NULL; pItem = ( CCLayer* )m_pItemsClaimTouch->objectAtIndex( i ); assert( NULL != pItem ); //window message items if( ( pItemWindowMessage = dynamic_cast< wmTouchDelegate* >( pItem ) ) ) { pItemWindowMessage->ccTouchCancelled( pTouch, pEvent ); } else//coscos2d-x items { pItem->ccTouchCancelled( pTouch, pEvent ); } } m_pItemsClaimTouch->removeAllObjects(); m_bDraging = false; } void wmTouchDelegate::passMessage( CCNode* pParent, CCTouch *pTouch, CCEvent *pEvent ) { if( !pParent ) { return; } //hande message to items int iNumChildren = 0; CCArray* pChildren = NULL; pChildren = pParent->getChildren(); //no children if( !pChildren ) { return; } iNumChildren = pParent->getChildren()->count(); //pass to all children for( int iChildIndex = 0; iChildIndex < iNumChildren; ++iChildIndex ) { wmTouchDelegate* pItemWindowMessage = NULL; CCNode* pItem = NULL; pItem = ( CCNode* )( pChildren->objectAtIndex( iChildIndex ) ); assert( pItem ); //if the item claims the touch message bool bClaim = false; //items derives from wmTouchDelegate if( ( pItemWindowMessage = dynamic_cast< wmTouchDelegate* >( pItem ) ) ) { bClaim = pItemWindowMessage->ccTouchBegan( pTouch, pEvent ); } else//items doesn't derive from wmTouchDelegate { //classes have non-trival ccTouchX() hander if( hasNonTrivalTouchHandler( pItem ) ) { bClaim = ( ( CCLayer* ) pItem )->ccTouchBegan( pTouch, pEvent ); } //items who doesn't derive from wmTouchDelegate can't pass touch message to its children, //so we have to help them to pass touch message. passMessage( pItem, pTouch, pEvent ); } //if this item is interested in this message, add it to array for other messages if( bClaim ) { m_pItemsClaimTouch->addObject( pItem ); } } } bool wmTouchDelegate::hasNonTrivalTouchHandler( cocos2d::CCNode *pItem ) { //classes have non-trival ccTouchX() hander const void* pItemAddress = &typeid( *pItem ); for ( int i = 0; i < sizeof( m_pNonTrivalTouchHandlerClasses ) / sizeof( void* ); ++i) { if( m_pNonTrivalTouchHandlerClasses[ i ] == pItemAddress ) { return true; } } return false; }其中比较重要的地方解释一下:
wmLayer, wmLayerAncestor, wmLayerDescendant, wmLayerModal都在一个头文件中完成,没有cpp。因为主要功能都有wmTouchDelegate完成了,这些类只是做了简单功能和约束的的添加。
wmLayer.h
// // Created by jason on 12-12-21. // // #ifndef __TableTest__WMLayer__ #define __TableTest__WMLayer__ #include <iostream> #include "cocos2d.h" #include "wmTouchDelegate.h" USING_NS_CC; #define WM_INIT_DEFAULT( parentClassName ) \ virtual bool init() \ { \ if( !parentClassName::init() ) \ { \ return false; \ } \ \ return true; \ } #define WM_TOUCH_REGISTER_DEFAULT( iPriority ) \ virtual void registerWithTouchDispatcher( void ) \ { \ CCDirector::sharedDirector()->getTouchDispatcher()->addTargetedDelegate( this, iPriority, true );\ } //Users shouldn't derive from wmLayerAncestor, wmLayerDescendant, wmLayerModal instead wmLayer. //wmLayer can't be touched. //wmLayer pass message to it's all descendant. class wmLayer : public CCLayer, public wmTouchDelegate { protected: WM_INIT_DEFAULT( CCLayer ); WM_TOUCH_DELEGATE_OWNER_CONSTRUCTOR_IN_HEAD_FILE( wmLayer ); WM_TOUCH_DELEGATE_IMPLEMENT_IN_HEAD_FILE(); //static public: CREATE_FUNC( wmLayer ); }; //wmLayerAncestor can be touched. //all secene should have only one wmLayerAncestor for bottom layer. //all the other layer should be wmLayerDescendant. class wmLayerAncestor : public wmLayer { protected: bool virtual init() { if( !wmLayer::init() ) { return false; } setTouchEnabled( true ); return true; } WM_TOUCH_REGISTER_DEFAULT( 0 ); //static public: CREATE_FUNC( wmLayerAncestor ); //data }; class wmLayerDescendant : public wmLayer { protected: WM_INIT_DEFAULT( wmLayer ); virtual void registerWithTouchDispatcher(){}; //static public: CREATE_FUNC( wmLayerDescendant ); //data }; //wmLayerModal stopes touch messages from being passed to other layers which are not it's children. //And will pass touch messages to it's children normally. class wmLayerModal : public wmLayerAncestor { protected: WM_INIT_DEFAULT( wmLayerAncestor ); WM_TOUCH_REGISTER_DEFAULT( kCCMenuHandlerPriority ); //static public: CREATE_FUNC( wmLayerModal ); //data }; #endif /* defined(__TableTest__WMLayer__) */里面通过一些宏,让我编写的更快一些。但希望没能阻碍你阅读。这里面没有太多需要说的。
如果你想让其他控件加入这个体制,简单的方法继承这些控件并屏蔽他们接受消息。如wmMenu,wmTableView,wmControl,代码如下:
//this menu won't get any touch message becase it doesn't register target touches. class wmMenu : public CCMenu { public: virtual void registerWithTouchDispatcher(){} static wmMenu * create(CCMenuItem* item, ...); };
//this UI can't derive from wmTouchDelegate due to it's CCTableViewCell Child. CCTableViewCell class wmTableView : public CCTableView { public: static wmTableView* create(cocos2d::extension::CCTableViewDataSource *dataSource, cocos2d::CCSize size, CCNode *container = NULL ); void registerWithTouchDispatcher(void){} };
class wmControlButton : public CCControlButton { public:
void registerWithTouchDispatcher(void){}static wmControlButton* create(CCNode* label, CCScale9Sprite* backgroundSprite) { wmControlButton *pRet = new wmControlButton(); pRet->initWithLabelAndBackgroundSprite(label, backgroundSprite); pRet->autorelease(); return pRet; }}; 只要让这些控件不接受消息就好了,我们的体制会确保消息能传递给他们身上的所有子控件。
但如果想更好的优化消息记录,应该避免一个wmLayer上面放N多层 非本体制控件(即没有继承wmTouchDelegate的控件)。可以在其中穿插一些本体制的控件,因为这些控件具有消息传递功能。如果创建本体制控件呢?
1.第一种情况,类对象已经继承了CCLayer,且没有自己重要的消息处理函数。如CCLayerColor,我们想把它变成本体系控件,我们应该继承CCLayerColor,并继承wmTouchDelegate。然后转接消 息处理函数。如下代码:
class wmLayerColor : public CCLayerColor, public wmTouchDelegate { protected: WM_TOUCH_DELEGATE_OWNER_CONSTRUCTOR_IN_HEAD_FILE( wmLayerColor ); WM_TOUCH_DELEGATE_IMPLEMENT_IN_HEAD_FILE(); WM_INIT_DEFAULT( CCLayerColor ); //static public: CREATE_FUNC( wmLayerColor ); };这是因为这类对象已经继承了CCLayer,我们不能让它再继承wmLayerDescendant,因为wmLayerDescendant也继承自ccLayer,这样多重继承中会有多份CCLayer,比较危险。所以我们简单的继承自wmTouchDelegate就可以了。
class wmUISellItem : public wmLayerDescendant { //macro public: enum { ITEM_NAME_LENGTH = 20, WIDTH = 219, HEIGHT = 141, }; //method protected: bool init(); //static public: static CGUISellItem* Create( const char* pszItemName, int iItemPriceNormal, int iItemPriceVIP, int iItemAmount ); };