C++
cocos2d-x

【cocos2dx】ScrollView内のGUIのタッチイベントに関する問題

More than 3 years have passed since last update.


ScrollViewがしんどいって話

ui名前空間にあるGUIたちはとっても優秀で,お手軽にcocos2dxでGUIを構築できる.

だがその中でもScrollViewくんがとっても問題児で.

ScrollViewは自分よりも大きいサイズのコンテンツを内含し,スクロール機能を付加するウィジェットである.

しかし,実際使ってみると不具合だらけなことに気がつく.

不具合ではないのかもしれないが,不都合な仕様であることに変わりはない.


  1. ScrollViewの外側の隠れて見えない内含コンテンツがタッチに反応する

  2. 内含コンテンツがタッチイベントをswallowするときスクロールできない

  3. スクロール後のタッチエンドイベントで特定のウィジェットが反応する

これは様々な人が抱えている問題のようで,非常に多くの改善案が提案されていた.

ただ,フレームワーク側を弄るのは論外

また,Nodeとしてのインターフェースを変更をしたくない

そして,極力コードを書きたくない

という欲求を満たす実装が見当たらなかったので,(多分)もっとも簡潔に不具合を全て解消する提案をする.

ScrollView自体を弄るのは何か違う気がしたので,中に追加するオブジェクトに工夫を加える.


実装

例えば,ui::Buttonの例を考える.

適当に継承する.最小限の構成は以下.

struct MyButton : public cocos2d::ui::Button{

public:
virtual bool init();
CREATE_FUNC(MyButton);
};

init関数を以下のように実装する.

bool MyButton::init(){

if(!ui::Button::init()){
return false;
}

_touchListener->setSwallowTouches(false);

_touchListener->onTouchBegan = [=](Touch* touch,Event* event){
Node* scrollview=nullptr;
for(Node* parent=getParent(); parent!=nullptr && scrollview==nullptr; parent=parent->getParent()){
scrollview = dynamic_cast<ui::ScrollView*>(parent);
}
if(scrollview && scrollview->getBoundingBox().containsPoint(touch->getLocation())){
return ui::Widget::onTouchBegan(touch, event);
}
else return true;
};

_touchListener->onTouchEnded = [=](Touch* touch,Event* event){
constexpr int leave_length = 10;
if((touch->getLocation() - touch->getStartLocation()).length() < leave_length){
ui::Widget::onTouchEnded(touch, event);
}
};
return true;
}

終わり.

早い話が親クラスのButtonのさらに親のWidgetのイベントリスナをハイジャックする.

_touchListenerはWidgetクラスのprotectedメンバだ.

まずボタンの上をタッチしてもスクロールを有効にするためにswallowを無効にする.

_touchListener->setSwallowTouches(false);

つぎにリスナのタッチイベントを乗っ取る.

その時に親クラスのScrollViewを探す.

Node* scrollview=nullptr;

for(Node* parent=getParent();
parent!=nullptr && scrollview==nullptr;
parent=parent->getParent()){
scrollview = dynamic_cast<ui::ScrollView*>(parent);
}

ScrollViewの親ノードが非null(ダウンキャストに成功)になるまで探索.

見つかったらcontainsPointでScrollViewからはみ出ていないか確かめる.

if(scrollview && scrollview->getBoundingBox().containsPoint(touch->getLocation())){

return ui::Widget::onTouchBegan(touch, event);
}

タッチエンドイベントでは,タッチのロケーションの変化量をチェックして適当なリテラルでイベントを通すかどうか決める.

ボタンをタッチするようなイベントはほぼ座標の変化しないタップのみで良いので低めにしている.

_touchListener->onTouchEnded = [=](Touch* touch,Event* event){

constexpr int leave_length = 10;
if((touch->getLocation() - touch->getStartLocation()).length() < leave_length){
ui::Widget::onTouchEnded(touch, event);
}
};

この実装はButtonに限らず自身でタッチリスナを定義するときにも使える.

ImageView等を配置するときに,上記のコードを書き加えれば良い.(親クラスにタッチイベントを伝播している部分を自分の実装に差し替える)

イベントリスナをローカルポインタ変数で作り参照を破棄してしまっているcocos2d::Menu等に関しては,以下の記事が参考になる.

http://qiita.com/noprops/items/b46134090a7024bd5a50