ScrollViewの中にMenuを入れた時の問題点
- Menuがタッチを飲み込んで、スクロールできない。
- スクロールが終わった時、最後に触れていたMenuItemがactivateされる。
- ScrollViewの見えないエリアにはみだしたMenuItemもタッチに反応してしまう。
これらを解決するために、Menuのサブクラスを作る。
#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を変更できるようにした。
#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のタッチイベントをキャンセルするようにする。
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を返した。
#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のアサーションによって
アプリが終了してしまうので、これらのメソッドを上書きする。
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);
とする。
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に渡すコールバックを変更
_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以上動いた時にコールバックを
呼ぶようにした。
class CC_GUI_DLL ScrollView : public Layout
{
//中略
//タッチ開始した時の_innerContainerの位置を覚えておく変数
Vec2 _scrollStartPosition;
}
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