Cocos2d-x(js)のccui.ListViewでリストメニューを作る

2016年10月25日(更新: 2016年12月2日)

Cocos2d-x を使ったアプリでリストメニューを作成する方法です。使用言語は javascript (Cocos2d-js) です。

ccui.ListView はiOSでいえば UITableView のようなリストメニューを作るための Cocos UI です。

以下のようにスワイプでスクロールするメニューが作れます。

ccui.ListViewの動作サンプル

項目が多くて画面スクロールが必要なメニューを作る際に便利です。

Cocos UI を使うための準備

ccui.ListView などの Cocos UI を使用する場合は、project.json の modules に extensions を加える必要があります。

...
"modules" : ["cocos2d", "extensions"],
...

not defined などのエラーが出る場合は、この設定が行われているか確認してください。

サンプルソースコード

実際に ccui.ListView を使ったシーンのソースコードです。

100個のリスト項目を作って縦に並べています。


// 色の設定
var color = {
    backgroundColor: cc.color(245, 255, 245),
    textColor: cc.color(0, 128, 255),
};

var UIListViewTest = cc.LayerColor.extend({
    showingListItemCount: 8,
    totalListItemCount: 100,
    totalListViewHeight: 0,
    listItemHeight: 240,
    itemMargin: 8,
    bufferZone: 240,
    reuseItemOffset: 0,
    lastContentPosY: 0,
    listView: null,
    ctor: function() {

        this._super(color.backgroundColor);

        var label = new cc.LabelTTF("ListViewテスト", "Arial", 60);
        label.setPosition(cc.winSize.width / 2, cc.winSize.height - 80);
        label.color = color.textColor;
        this.addChild(label);

        this.itemArray = [];
        for (var i = 0; i < this.totalListItemCount; ++i) {
            this.itemArray.push("リストアイテム " + i);
        }

        // ListViewを作成
        this.listView = new ccui.ListView();
        this.listView.setDirection(ccui.ScrollView.DIR_VERTICAL);
        this.listView.setTouchEnabled(true);
        this.listView.setBounceEnabled(true);
        this.listView.setContentSize(cc.size(cc.winSize.width, cc.winSize.height - 160));

        // ListViewの基準点は左下
        this.listView.x = 0;
        this.listView.y = 0;

        // ListViewにタッチイベントを付与
        this.listView.addEventListener(this.selectedItemEvent, this);

        this.listView.setGravity(ccui.ListView.GRAVITY_CENTER_VERTICAL);
        this.addChild(this.listView);

        // リスト項目のテンプレートを作る
        var listItemModel = new ccui.Layout();
        listItemModel.setTouchEnabled(true);
        listItemModel.setBackGroundColorType(ccui.Layout.BG_COLOR_SOLID);
        listItemModel.setBackGroundColor(cc.color(200, 200, 200));
        listItemModel.setContentSize(cc.size(cc.winSize.width, this.listItemHeight));

        // リスト項目に載せるボタン
        var listButton = new ccui.Button();
        listButton.setName("ButtonItem");
        listButton.titleFontSize = 64;
        listButton.setTouchEnabled(true);
        // listButton.loadTextures(res.button, res.button_selected, "");
        listButton.x = listItemModel.width / 2;
        listButton.y = listItemModel.height / 2;

        listItemModel.addChild(listButton);
        this.listView.setItemModel(listItemModel);

        // はじめに表示する項目はコンストラクタ内で作っておく
        for (i = 0; i < this.showingListItemCount; ++i) {
            var item = listItemModel.clone();
            item.setTag(i);
            var button = item.getChildByName('ButtonItem');
            button.setTitleText(this.itemArray[i]);
            button.setTitleColor(color.textColor);
            this.listView.pushBackCustomItem(item);
        }

        this.listView.setItemsMargin(this.itemMargin);
        this.reuseItemOffset = (this.listItemHeight + this.itemMargin) * this.showingListItemCount;

        this.scheduleUpdate();

        return true;
    },
    onEnter: function() {

        cc.log("onEnter");
        // cc.Node の onEnter をこのオブジェクトを this として呼び出す
        cc.Node.prototype.onEnter.call(this);

        // onEnter で forceDoLayout を呼び出す必要あり
        this.listView.forceDoLayout();

        // ListViewの大きさ設定
        this.totalListViewHeight = this.listItemHeight * this.totalListItemCount + (this.totalListItemCount - 1) * this.itemMargin;
        this.listView.getInnerContainer().setContentSize(cc.size(this.listView.getInnerContainerSize().width, this.totalListViewHeight));

        this.listView.jumpToTop();
    },
    getItemPositionYInView: function(item) {
        var worldPos = item.getParent().convertToWorldSpaceAR(item.getPosition());
        var viewPos = this.listView.convertToNodeSpaceAR(worldPos);
        return viewPos.y;
    },
    updateItem: function(itemID, templateID) {
        var itemTemplate = this.listView.getItems()[templateID];
        var button = itemTemplate.getChildByName('ButtonItem');
        itemTemplate.setTag(itemID);
        button.setTitleText(this.itemArray[itemID]);
    },
    update: function(dt) {

        var listViewHeight = this.listView.getContentSize().height;
        var items = this.listView.getItems();
        var isScrollUp = this.listView.getInnerContainer().getPosition().y < this.lastContentPosY;

        // リスト項目の更新
        for (var i = 0; i < this.showingListItemCount && i < this.totalListItemCount; ++i) {

            var item = items[i];
            var itemPosY = this.getItemPositionYInView(item);

            if (isScrollUp) {
                // 画面が上にスクロールしているとき(リスト項目が下にスクロールしているとき)
                if (itemPosY < -this.bufferZone && item.getPosition().y + this.reuseItemOffset < this.totalListViewHeight) {
                    item.setPositionY(item.getPositionY() + this.reuseItemOffset);
                    var itemID = item.getTag() - items.length;
                    this.updateItem(itemID, i);
                }
            } else {
                if (itemPosY > this.bufferZone + listViewHeight && item.getPositionY() - this.reuseItemOffset >= 0) {
                    item.setPositionY(item.getPositionY() - this.reuseItemOffset);
                    itemID = item.getTag() + items.length;
                    this.updateItem(itemID, i);
                }
            }
        }

        this.lastContentPosY = this.listView.getInnerContainer().getPosition().y;
    },
    selectedItemEvent: function(sender, type) {
        // リスト項目をタップした時の処理
        switch (type) {
            case ccui.ListView.EVENT_SELECTED_ITEM:
                // リスト項目をタップした瞬間
                // スクロールさせたい時も反応してしまうので、イベントは下の ON_SELECTED_ITEM_END につけるべき
                break;

            case ccui.ListView.ON_SELECTED_ITEM_END:
                var targetListView = sender;
                var item = targetListView.getItem(targetListView.getCurSelectedIndex());
                cc.log("Item: " + item.getTag());
                break;
            default:
                break;
        }
    }
});

var ListViewTestScene = cc.Scene.extend({
    onEnter: function() {
        this._super();
        this.addChild(new UIListViewTest());
    }
});

補足説明

リスト項目の表示数は余裕を持たせる

表示するリストの項目数(サンプルコード内では変数 showingListItemCount で設定)は、実際に表示できる項目数よりも多めにしておいた方が、スクロールの際、項目が作成された瞬間をユーザーに見られる心配がありません。

これをギリギリにしていると、速くスクロールした時にリスト項目が消えたり現れたりするところが見えてしまいます

同じ理由で、リスト項目がどれだけ画面の表示領域に近づいたらリスト項目を作成するかを設定している変数 bufferZone も余裕を持った大きめの値にしておくといいでしょう。

リスト項目はスクロールに合わせて再構成される

画面から出たリスト項目はリセットされるため、再度画面に表示される際に中身を再度設定する必要があります。このサンプルでは関数 updateItem でリスト項目内のボタンのテキストや位置の再調整を行なっています。

このスライド時のリスト項目の更新(削除・作成)は、毎フレームごとに関数 update が状態を監視して必要なときに行います。

リスト項目に画像を使用する

リスト項目に画像を使いたい場合は、コメントアウトしてある ccui.Button のメソッド loadTextures を設定してください。このメソッドの引数は以下のようになっています。

loadTextures("通常時の画像のパス", "タップ時の画像のパス", "使用不能時の画像のパス");

ListViewのタッチイベント

ユーザーがリストをスライドさせる際、リスト項目(ボタン)の上に指を置いてスワイプすることが多いとおもいます。

スワイプしようとしたのにボタンが反応してしまってはいけないので、スライドせずに指を離した場合だけボタンが反応するようにする必要があります。

したがって、イベントタイプは ccui.ListView.EVENT_SELECTED_ITEM ではなく ccui.ListView.ON_SELECTED_ITEM_END を受け取るようにしたほうがよいかと思います。

(Cocosのサンプルコードでは ccui.ListView.EVENT_SELECTED_ITEM でイベント処理を行っています)

以上が ccui.ListView の基本的な使い方です。

コメントを残す

メールアドレスが公開されることはありません。