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

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

1. Menuがタッチを飲み込んで、スクロールできない。
2. スクロールが終わった時、最後に触れていたMenuItemがactivateされる。
3. ScrollViewの見えないエリアにはみだしたMenuItemもタッチに反応してしまう。

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

```lang: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を変更できるようにした。

```lang: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のタッチイベントをキャンセルするようにする。

```lang: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を返した。

```lang: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のアサーションによって
アプリが終了してしまうので、これらのメソッドを上書きする。

```lang: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);
とする。

```lang: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に渡すコールバックを変更

```lang: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以上動いた時にコールバックを
呼ぶようにした。

```lang:UIScrollView.h
class CC_GUI_DLL ScrollView : public Layout
{
    //中略
    //タッチ開始した時の_innerContainerの位置を覚えておく変数
    Vec2 _scrollStartPosition;
}
```
```lang: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