Edited at

cocos2d-x v3 ScrollViewの中にMenuを入れる

More than 3 years have passed since last update.


ScrollViewの中にMenuを入れた時の問題点


  1. Menuがタッチを飲み込んで、スクロールできない。

  2. スクロールが終わった時、最後に触れていたMenuItemがactivateされる。

  3. ScrollViewの見えないエリアにはみだしたMenuItemもタッチに反応してしまう。

これらを解決するために、Menuのサブクラスを作る。


MyMenu.h

#ifndef __MyMenu__

#define __MyMenu__

#include "cocos2d.h"
#include "ui/CocosGUI.h"

USING_NS_CC;
using namespace ui;

class MyMenu : public Menu
{
public:
MyMenu();
~MyMenu();
static MyMenu* create();
static MyMenu* create(MenuItem* item, ...) CC_REQUIRES_NULL_TERMINATION;
static MyMenu* createWithItems(MenuItem* item, va_list args);
static MyMenu* createWithArray(const Vector<MenuItem*>& arrayOfItems);

virtual bool init();
virtual bool initWithArray(const Vector<MenuItem*>& arrayOfItems);

void setScrollView(ScrollView* scrollView);

virtual bool onTouchBegan(Touch* touch, Event* event) override;
virtual void onTouchEnded(Touch* touch, Event* event) override;
virtual void onTouchCancelled(Touch* touch, Event* event) override;
virtual void onTouchMoved(Touch* touch, Event* event) override;
protected:
private:
EventListenerTouchOneByOne* _listener;
ScrollView* _scrollView;
};

#endif


EventListenerをメンバ変数にすることによって、

後からswallowTouchesを変更できるようにした。


MyMenu.cpp

#include "MyMenu.h"

MyMenu::MyMenu()
:_listener(nullptr),
_scrollView(nullptr)
{

}

MyMenu::~MyMenu()
{
CC_SAFE_RELEASE_NULL(_listener);
}

#pragma mark - create methods

MyMenu* MyMenu::create()
{
return MyMenu::create(nullptr, nullptr);
}

MyMenu* MyMenu::create(MenuItem* item, ...)
{
va_list args;
va_start(args,item);

MyMenu *ret = MyMenu::createWithItems(item, args);

va_end(args);

return ret;
}

MyMenu* MyMenu::createWithItems(MenuItem* item, va_list args)
{
Vector<MenuItem*> items;
if( item )
{
items.pushBack(item);
MenuItem *i = va_arg(args, MenuItem*);
while(i)
{
items.pushBack(i);
i = va_arg(args, MenuItem*);
}
}

return MyMenu::createWithArray(items);
}

MyMenu* MyMenu::createWithArray(const Vector<MenuItem*>& arrayOfItems)
{
auto ret = new (std::nothrow) MyMenu();
if (ret && ret->initWithArray(arrayOfItems))
{
ret->autorelease();
}
else
{
CC_SAFE_DELETE(ret);
}

return ret;
}

#pragma mark - initializer

bool MyMenu::init()
{
return initWithArray(Vector<MenuItem*>());
}

bool MyMenu::initWithArray(const Vector<MenuItem*>& arrayOfItems)
{
if (Layer::init())
{
_enabled = true;

ignoreAnchorPointForPosition(false);
setContentSize(Size(0,0));

int z=0;

for (auto& item : arrayOfItems)
{
this->addChild(item, z);
z++;
}

_selectedItem = nullptr;
_state = Menu::State::WAITING;

// enable cascade color and opacity on menus
setCascadeColorEnabled(true);
setCascadeOpacityEnabled(true);

_listener = EventListenerTouchOneByOne::create();
CC_SAFE_RETAIN(_listener);
_listener->setSwallowTouches(true);

_listener->onTouchBegan = CC_CALLBACK_2(Menu::onTouchBegan, this);
_listener->onTouchMoved = CC_CALLBACK_2(Menu::onTouchMoved, this);
_listener->onTouchEnded = CC_CALLBACK_2(Menu::onTouchEnded, this);
_listener->onTouchCancelled = CC_CALLBACK_2(Menu::onTouchCancelled, this);

_eventDispatcher->addEventListenerWithSceneGraphPriority(_listener, this);

return true;
} else {
return false;
}
}


ScrollViewをセットする時、swallowTouchesをfalseにして、

ScrollViewにタッチイベントを渡せるようにする。

また、ScrollViewがスクロールした時、Menuのタッチイベントをキャンセルするようにする。


MyMenu.cpp

void MyMenu::setScrollView(ScrollView* scrollView)

{
_scrollView = scrollView;
_listener->setSwallowTouches(false);
_scrollView->addEventListener([this](Ref* ref, ScrollView::EventType eventType) {
if (eventType == ScrollView::EventType::CONTAINER_MOVED) {
this->onTouchCancelled(nullptr, nullptr);
}
});
}

onTouchBeganでは、タッチがScrollViewの中にあるか調べて、

そうでなければfalseを返した。


MyMenu.cpp

#pragma mark - touch

bool MyMenu::onTouchBegan(Touch* touch, Event* event)
{
if (_scrollView) {
Point touchPoint = touch->getLocation();
Rect rect = Rect(_scrollView->convertToWorldSpace(_scrollView->getPosition()),
_scrollView->getContentSize());
if (rect.containsPoint(touchPoint)) {
return Menu::onTouchBegan(touch, event);
} else {
return false;
}
} else {
return Menu::onTouchBegan(touch, event);
}
}


ScrollViewがスクロールした時、Menuのタッチイベントがキャンセルされるが、

その後、onTouchMovedやonTouchEndedが呼ばれると、Menuのアサーションによって

アプリが終了してしまうので、これらのメソッドを上書きする。


MyMenu.cpp

void MyMenu::onTouchEnded(Touch* touch, Event* event)

{
if (_state == Menu::State::TRACKING_TOUCH) {
Menu::onTouchEnded(touch, event);
}
}

void MyMenu::onTouchCancelled(Touch* touch, Event* event)
{
if (_state == Menu::State::TRACKING_TOUCH) {
Menu::onTouchCancelled(touch, event);
}
}

void MyMenu::onTouchMoved(Touch* touch, Event* event)
{
if (_state == Menu::State::TRACKING_TOUCH) {
Menu::onTouchMoved(touch, event);
}
}



使い方

MyMenuをScrollViewに入れて、

menu->setScrollView(scrollView);

とする。


HelloWorldScene.cpp

MyMenu* menu = MyMenu::create();

for (int i = 0; i < 30; i++) {
Label* label = Label::createWithSystemFont(StringUtils::format("TestString%d", i),
"Helvetica",
20);
MenuItemLabel* item = MenuItemLabel::create(label, [i](Ref* sender) {
CCLOG("item pressed : %d", i);
});
menu->addChild(item);
}
menu->alignItemsVertically();
menu->setContentSize(size);

ScrollView* scrollView = ScrollView::create();
scrollView->addChild(menu);
scrollView->setInnerContainerSize(menu->getContentSize());
scrollView->setContentSize(Size(100,100));
scrollView->setDirection(ScrollView::Direction::VERTICAL);

menu->setScrollView(scrollView);


この方法だと、ScrollViewのサブクラスは作らずに、解決する。

追記:

MenuからScrollViewに渡すコールバックを変更


MyMenu.cpp

_scrollView->addEventListener([this](Ref* ref, ScrollView::EventType eventType) {

//if (eventType == ScrollView::EventType::SCROLLING) {
if (eventType == ScrollView::EventType::CONTAINER_MOVED) {
this->onTouchCancelled(nullptr, nullptr);
}
});

また、この方法だと、Xperiaなどの一部のAndroid端末で、

タッチの微小な動きがスクロールと感知されてしまい、

ボタンのタッチが全てキャンセルされてしまいボタンが押せないという問題が起こった。

ScrollViewのコードを変更して、_innerContainerが20以上動いた時にコールバックを

呼ぶようにした。


UIScrollView.h

class CC_GUI_DLL ScrollView : public Layout

{
//中略
//タッチ開始した時の_innerContainerの位置を覚えておく変数
Vec2 _scrollStartPosition;
}


UIScrollView.cpp

bool ScrollView::onTouchBegan(Touch *touch, Event *unusedEvent)

{
//edit
_scrollStartPosition = _innerContainer->getPosition();

bool pass = Layout::onTouchBegan(touch, unusedEvent);
if (!_isInterceptTouch)
{
if (_hitted)
{
handlePressLogic(touch);
}
}
return pass;
}
void ScrollView::setInnerContainerPosition(const Vec2 &position)
{
if(position == _innerContainer->getPosition())
{
return;
}
_innerContainer->setPosition(position);
_outOfBoundaryAmountDirty = true;

// Process bouncing events
if(_bounceEnabled)
{
for(int direction = (int) MoveDirection::TOP; direction < (int) MoveDirection::RIGHT; ++direction)
{
if(isOutOfBoundary((MoveDirection) direction))
{
processScrollEvent((MoveDirection) direction, true);
}
}
}

this->retain();
//if (_eventCallback)
//edit
if (_eventCallback && _scrollStartPosition.distanceSquared(_innerContainer->getPosition()) > 400)
{
_eventCallback(this, EventType::CONTAINER_MOVED);
}
if (_ccEventCallback)
{
//_ccEventCallback(this, static_cast<int>(EventType::CONTAINER_MOVED));
}
this->release();
}


参考:

http://qiita.com/qittu/items/88b53597eda287f7fa70

http://lethargysyndrome.blog.fc2.com/blog-entry-10.html