cocos 2dxAndroid技术知识Android开发

cocos2d-x-js(cocos2d-x-3.17) 项目经

2021-04-09  本文已影响0人  Kevinmob

背景及介绍

cocos2d-x-js的安装和基本用法本文不做讨论,主要总结一下于项目相关的经验。
项目是一款针对学龄前和小学生的汉语学习移动端app(Android、iOS),里面会用到一些动画特效和粒子特殊,经过一番调研最终采用了cocos2d-x-js (2019年4月到2021年4月),经过了前期的环境配置和安装,我们来到了api学习和熟悉阶段,借助官方提供的“吃寿司”小demo以及官方文档,我们快速的熟悉了cocos2d-x-js的一些基本用法,之后便来到了大量的需求实现阶段,下面是在需求实现中多次试错后取得的成果。

frameworks代码已上传到github https://github.com/kevin-mob/cocos2djs

屏幕适配

设计图以iPhone6的屏幕尺寸为标准进行设计,并通过动态计算缩放宽高最终达到全屏尺寸并去除黑边的效果。

相关计算代码

    // Uncomment the following line to set a fixed orientation for your game
    cc.view.setOrientation(cc.ORIENTATION_LANDSCAPE);

    // Setup the resolution policy and design resolution size
    var referenceSize=cc.size(1334, 750);

    var screenSize = cc.view.getFrameSize();
    var scale_x = screenSize.width/referenceSize.width;
    var scale_y = screenSize.height/referenceSize.height;

    // cc.ResolutionPolicy.SHOW_ALL适配保证内容完全展示,对(宽或高的)黑边区域进行缩放处理
    // 当前屏幕宽高比小于设计分辨率(1334*750),例如设备iPad,会出现上下黑边,需要对高度进行放大到全屏处理。
    if (scale_x<scale_y){
        var resolutionSize = cc.size(referenceSize.width,screenSize.height/scale_x);
        cc.view.setDesignResolutionSize(resolutionSize.width,resolutionSize.height, cc.ResolutionPolicy.SHOW_ALL);
    }else {
        //当前屏幕宽高比大于设计分辨率(1334*750),例如设备iPhoneX,会出现左右黑边,需要对度进行放大到全屏处理。
        var resolutionSize = cc.size(screenSize.width/scale_y, referenceSize.height);
        cc.view.setDesignResolutionSize(resolutionSize.width,resolutionSize.height, cc.ResolutionPolicy.SHOW_ALL);
    }
    cc.Device.setKeepScreenOn(true);

全部main.js代码

    /****************************************************************************
 Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd.

 http://www.cocos2d-x.org

 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights
 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:

 The above copyright notice and this permission notice shall be included in
 all copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
 ****************************************************************************/

/**
 * A brief explanation for "project.json":
 * Here is the content of project.json file, this is the global configuration for your game, you can modify it to customize some behavior.
 * The detail of each field is under it.
 {
    "project_type": "javascript",
    // "project_type" indicate the program language of your project, you can ignore this field

    "debugMode"     : 1,
    // "debugMode" possible values :
    //      0 - No message will be printed.
    //      1 - cc.error, cc.assert, cc.warn, cc.log will print in console.
    //      2 - cc.error, cc.assert, cc.warn will print in console.
    //      3 - cc.error, cc.assert will print in console.
    //      4 - cc.error, cc.assert, cc.warn, cc.log will print on canvas, available only on web.
    //      5 - cc.error, cc.assert, cc.warn will print on canvas, available only on web.
    //      6 - cc.error, cc.assert will print on canvas, available only on web.

    "showFPS"       : true,
    // Left bottom corner fps information will show when "showFPS" equals true, otherwise it will be hide.

    "frameRate"     : 60,
    // "frameRate" set the wanted frame rate for your game, but the real fps depends on your game implementation and the running environment.

    "noCache"       : false,
    // "noCache" set whether your resources will be loaded with a timestamp suffix in the url.
    // In this way, your resources will be force updated even if the browser holds a cache of it.
    // It's very useful for mobile browser debugging.

    "id"            : "gameCanvas",
    // "gameCanvas" sets the id of your canvas element on the web page, it's useful only on web.

    "renderMode"    : 0,
    // "renderMode" sets the renderer type, only useful on web :
    //      0 - Automatically chosen by engine
    //      1 - Forced to use canvas renderer
    //      2 - Forced to use WebGL renderer, but this will be ignored on mobile browsers

    "engineDir"     : "frameworks/cocos2d-html5/",
    // In debug mode, if you use the whole engine to develop your game, you should specify its relative path with "engineDir",
    // but if you are using a single engine file, you can ignore it.

    "modules"       : ["cocos2d"],
    // "modules" defines which modules you will need in your game, it's useful only on web,
    // using this can greatly reduce your game's resource size, and the cocos console tool can package your game with only the modules you set.
    // For details about modules definitions, you can refer to "../../frameworks/cocos2d-html5/modulesConfig.json".

    "jsList"        : [
    ]
    // "jsList" sets the list of js files in your game.
 }
 *
 */

cc.game.onStart = function () {
    var sys = cc.sys;
    if (!sys.isNative && document.getElementById("cocosLoading")) //If referenced loading.js, please remove it
        document.body.removeChild(document.getElementById("cocosLoading"));

    // Pass true to enable retina display, on Android disabled by default to improve performance
    cc.view.enableRetina(sys.os === sys.OS_IOS ? true : false);

    // Disable auto full screen on baidu and wechat, you might also want to eliminate sys.BROWSER_TYPE_MOBILE_QQ
    if (sys.isMobile &&
        sys.browserType !== sys.BROWSER_TYPE_BAIDU &&
        sys.browserType !== sys.BROWSER_TYPE_WECHAT) {
        cc.view.enableAutoFullScreen(true);
    }

    // Adjust viewport meta
    cc.view.adjustViewPort(true);

    // Uncomment the following line to set a fixed orientation for your game
    cc.view.setOrientation(cc.ORIENTATION_LANDSCAPE);

    // Setup the resolution policy and design resolution size
    var referenceSize=cc.size(1334, 750);

    var screenSize = cc.view.getFrameSize();
    var scale_x = screenSize.width/referenceSize.width;
    var scale_y = screenSize.height/referenceSize.height;

    // cc.ResolutionPolicy.SHOW_ALL适配保证内容完全展示,对(宽或高的)黑边区域进行缩放处理
    // 当前屏幕宽高比小于设计分辨率(1334*750),例如设备iPad,会出现上下黑边,需要对高度进行放大到全屏处理。
    if (scale_x<scale_y){
        var resolutionSize = cc.size(referenceSize.width,screenSize.height/scale_x);
        cc.view.setDesignResolutionSize(resolutionSize.width,resolutionSize.height, cc.ResolutionPolicy.SHOW_ALL);
    }else {
        //当前屏幕宽高比大于设计分辨率(1334*750),例如设备iPhoneX,会出现左右黑边,需要对度进行放大到全屏处理。
        var resolutionSize = cc.size(screenSize.width/scale_y, referenceSize.height);
        cc.view.setDesignResolutionSize(resolutionSize.width,resolutionSize.height, cc.ResolutionPolicy.SHOW_ALL);
    }
    cc.Device.setKeepScreenOn(true);

    // The game will be resized when browser size change
    cc.view.resizeWithBrowserSize(true);

    //load resources
    cc.LoaderScene.preload(g_resources, function () {
        cc.director.runScene(new SXTHomePageScene());
    }, this);
};
cc.game.run();

列表item复用

cocos2d-x-js(cocos2d-x-3.17)的demo中提供了一个用于展示列表数据的demo位置在GUITest>UIListViewTest中,简单少量的数据展示可以使用,但是数据量一旦变多在使用的话会出现大量占用内存导致的卡顿甚至是闪退,原因是demo中并没有ui对象复用的逻辑,经过了一番逻辑梳理,结合android中ListView的绘制原理,对该组件做了大量的优化改进工作,最终解决了卡顿、闪退的问题。
全部代码

var BaseListLayer = cc.Layer.extend({
    _spawnCount: 10,
    _totalCount: 0,
    _bufferZone: 50,
    _updateInterval: 0.1,
    _spacing: 0,
    _updateTimer: 0,
    _lastContentPosY: 0,
    _lastContentPosX: 0,
    _reuseItemOffset: 0,
    _initializeListSize: false,
    listView: null,
    defaultItem: null,
    direction: null,
    _array: [],
    _listViewLayoutInfo: [],
    _isReEnter: false,
    _listViewInnerContainerLastPosition:null,
    ctor: function () {
        this._super();

        // Create the list view
        this.listView = new ccui.ListView();
        this.listView.setTouchEnabled(true);
        this.listView.setBounceEnabled(true);
        this.listView.addEventListener(this.selectedItemEvent.bind(this));

        // set all items layout gravity
        this.listView.setGravity(ccui.ListView.GRAVITY_CENTER_VERTICAL);
        this.setupListView(this.listView);

        this.direction = this.listView.getLayoutType();
        this.addChild(this.listView);

        // create model
        this.defaultItem = new ccui.Layout();
        this.defaultItem.setTouchEnabled(true);

        this.setupItemModel(this.defaultItem);

        // set model
        this.listView.setItemModel(this.defaultItem);

        this.listView.setItemsMargin(this._spacing);
        if (this.direction == ccui.ScrollView.DIR_VERTICAL) {
            this._itemTemplateHeight = this.defaultItem.getContentSize().height;

            this._reuseItemOffset = (this._itemTemplateHeight + this._spacing) * this._spawnCount;
        } else if (this.direction == ccui.ScrollView.DIR_HORIZONTAL) {
            this._itemTemplateWidth = this.defaultItem.getContentSize().width;
            // FIXME 复用的偏移量为 原始_spawnCount 个view的宽度之和,可以改为根据ListView宽度自动计算复用宽度和_spawnCount,无需外部指定_spawnCount个数
            this._reuseItemOffset = (this._itemTemplateWidth + this._spacing) * this._spawnCount;
        }
    },

    /**
     *
     * @param listView {ccui.ListView}
     */
    setupListView: function (listView) {
        throw new Error("use BaseListLayer need override setupListView")
    },

    /**
     *  listView 默认的item模板
     * @param defaultItem {ccui.Layout}
     */
    setupItemModel: function (defaultItem) {
        throw new Error("use BaseListLayer need override setupItemModel")
    },

    /**
     * 进行itemLayout和数据绑定操作
     * @param itemLayout {ccui.Layout}
     * @param dataArray
     * @param index
     */
    onSetupItemData: function (itemLayout, dataArray, index) {
        throw new Error("use BaseListLayer need override onSetupItemData method")
    },

    setOnItemClickCallback: function (onItemClickCallback) {
        this.onItemClickCallback = onItemClickCallback;
    },

    setData: function (array) {
        this._isReEnter = false;
        this.listView.removeAllChildren();
        this._lastContentPosY = 0;
        this._lastContentPosX = 0;
        this._totalCount = 0;
        this.unscheduleUpdate();
        this._array = array;
        // 填充原始view
        for (let i = 0; i < array.length; i++) {
            // 超过_spawnCount数量的数据后停止预渲染
            if (i < this._spawnCount) {
                let item = new ccui.Layout();
                this.setupItemModel(item);
                item.setTag(i);
                this.onSetupItemData(item, array, i);
                this.listView.pushBackCustomItem(item);
            } else {
                break;
            }
        }
        this._totalCount = this._array.length;

        if (this.direction == ccui.ScrollView.DIR_VERTICAL) {
            let totalHeight = this._itemTemplateHeight * this._totalCount +
                (this._totalCount - 1) * this._spacing +
                this.listView.getTopPadding() + this.listView.getBottomPadding();
            if (totalHeight > this.listView.getContentSize().height) {
                this.listView.forceDoLayout();
                this.listView.getInnerContainer().setContentSize(cc.size(this.listView.getInnerContainerSize().width, totalHeight));
                //更新数据 移动内容到最前面
                this.listView.jumpToTop();
            }
        } else if (this.direction == ccui.ScrollView.DIR_HORIZONTAL) {
            let totalWidth = this._itemTemplateWidth * this._totalCount +
                (this._totalCount - 1) * this._spacing +
                this.listView.getLeftPadding() + this.listView.getRightPadding();
            if (totalWidth > this.listView.getContentSize().width) {
                this.listView.forceDoLayout();
                this.listView.getInnerContainer().setContentSize(cc.size(totalWidth, this.listView.getInnerContainerSize().height));
                //更新数据 移动内容到最前面
                this.listView.jumpToTop();
            }
        }

        this.scheduleUpdate();
    },

    getItemPositionYInView: function (item) {
        var worldPos = item.getParent().convertToWorldSpaceAR(item.getPosition());
        var viewPos = this.listView.convertToNodeSpaceAR(worldPos);
        return viewPos.y;
    },
    getItemPositionXInView: function (item) {
        var worldPos = item.getParent().convertToWorldSpaceAR(item.getPosition());
        var viewPos = this.listView.convertToNodeSpaceAR(worldPos);
        return viewPos.x;
    },

    update: function (dt) {
        this._updateTimer += dt;
        if (this._updateTimer < this._updateInterval) {
            return;
        }

        if(this._isReEnter)
            return;

        if (this.direction == ccui.ScrollView.DIR_VERTICAL) {
            this.updateVerticalList();
        } else if (this.direction == ccui.ScrollView.DIR_HORIZONTAL) {
            this.updateHorizontalList();
        }
    },

    updateVerticalList: function () {
        if (this.listView.getInnerContainer().getPosition().y === this._lastContentPosY) {
            return;
        }
        this._updateTimer = 0;

        var totalHeight = this._itemTemplateHeight * this._totalCount + (this._totalCount - 1) * this._spacing;
        var listViewHeight = this.listView.getContentSize().height;
        var items = this.listView.getItems();

        let itemCount = items.length;

        //手势的滑动方向
        var isDown = this.listView.getInnerContainer().getPosition().y < this._lastContentPosY;

        let itemID;
        for (var i = 0; i < itemCount && i < this._totalCount; ++i) {
            var item = items[i];
            var itemPos = this.getItemPositionYInView(item);
            if (isDown) {
                if (itemPos < -this._bufferZone - this.defaultItem.height && item.getPosition().y + this._reuseItemOffset < totalHeight) {
                    itemID = item.getTag() - itemCount;
                    cc.log("====== 下滑 itemID " + itemID);
                    item.setPositionY(item.getPositionY() + this._reuseItemOffset);
                    this.updateItem(itemID, i);
                }
            } else {
                if (itemPos > this._bufferZone + listViewHeight && item.getPositionY() - this._reuseItemOffset >= 0) {
                    item.setPositionY(item.getPositionY() - this._reuseItemOffset);
                    itemID = item.getTag() + itemCount;
                    cc.log("====== 上滑 itemID " + itemID);
                    this.updateItem(itemID, i);
                }
            }
        }
        this._lastContentPosY = this.listView.getInnerContainer().getPosition().y;
    },

    updateHorizontalList: function () {

        if (this.listView.getInnerContainer().getPosition().x === this._lastContentPosX) {
            return;
        }

        this._updateTimer = 0;

        var totalWidth = this._itemTemplateWidth * this._totalCount + (this._totalCount - 1) * this._spacing;
        var items = this.listView.getItems();

        // 屏幕在内容上的移动方向
        var isRight = this.listView.getInnerContainer().getPosition().x < this._lastContentPosX;
        // jumpToItem时,计算几倍重用
        var moveMultiple = Math.abs((this.listView.getInnerContainer().getPosition().x - this._lastContentPosX) / this._reuseItemOffset);
        moveMultiple = Math.ceil(moveMultiple);

        // 缓冲区设为4个模板view的宽度
        this._bufferZone = this._itemTemplateWidth * 4;

        if (isRight) {
            if (moveMultiple > 1) {
                // 跳跃式更新时(单次刷新x移动超过一屏),先刷新目标屏幕的前一屏数据,再刷新目标屏数据,保证显示没有空白
                this._ascendUpdate(moveMultiple - 1, totalWidth, items);
                this._ascendUpdate(1, totalWidth, items);
            } else {
                this._ascendUpdate(moveMultiple, totalWidth, items);
            }
        } else {
            if (moveMultiple > 1) {
                // 跳跃式更新时(单次刷新x移动超过一屏),先刷新目标屏幕的前一屏数据,再刷新目标屏数据,保证显示没有空白
                this._descendUpdate(moveMultiple - 1, totalWidth, items);
                this._descendUpdate(1, totalWidth, items);
            } else {
                this._descendUpdate(moveMultiple, totalWidth, items);
            }
        }
        this._lastContentPosX = this.listView.getInnerContainer().getPosition().x;
    },

    // 从左向右更新view,复用左边超出缓冲区的view
    _ascendUpdate: function (moveMultiple, totalWidth, items) {
        let dataIndex;
        let item;
        let itemPos;
        let itemCount = items.length;
        // 遍历items找到缓冲区左边的view进行复用, 计算的最终PositionX超过右边界停止更新,列表到头了
        for (let i = 0; i < itemCount && i < this._totalCount; i++) {
            item = items[i];
            itemPos = this.getItemPositionXInView(item);
            //找到缓冲区外面的view进行复用并且判断是否超出了总区域的右边界
            if (itemPos < -this._bufferZone && item.getPosition().x + this._reuseItemOffset * moveMultiple < totalWidth) {
                dataIndex = item.getTag() + itemCount * moveMultiple;
                item.setPositionX(item.getPositionX() + this._reuseItemOffset * moveMultiple);
                this.updateItem(dataIndex, i);
            }
        }
    },

    //从右向左更新view,复用右边超出缓冲区的view
    _descendUpdate: function (moveMultiple, totalWidth, items) {
        let dataIndex;
        let item;
        let itemPos;
        let itemCount = items.length;
        let listViewWidth = this.listView.getContentSize().width;
        // 遍历items找到缓冲区右边的view进行复用, 计算的最终PositionX超过左边界停止更新,列表到头了
        for (let i = Math.min(itemCount, this._totalCount) - 1; i >= 0; i--) {
            item = items[i];
            itemPos = this.getItemPositionXInView(item);
            //找到缓冲区右边的view进行复用,并且判断是否超出了总区域的左边界
            if (itemPos > this._bufferZone + listViewWidth && item.getPositionX() - this._reuseItemOffset * moveMultiple >= 0) {
                item.setPositionX(item.getPositionX() - this._reuseItemOffset * moveMultiple);
                dataIndex = item.getTag() - itemCount * moveMultiple;
                this.updateItem(dataIndex, i);
            }
        }
    },

    updateAllItem: function () {
        let items = this.listView.getItems();
        let item;
        let itemCount = items.length;
        for (let i = Math.min(itemCount, this._totalCount) - 1; i >= 0; i--) {
            item = items[i];
            this.onSetupItemData(item, this._array, item.getTag());
        }
    },

    updateItem: function (dataIndex, templateIndex) {
        var itemTemplate = this.listView.getItems()[templateIndex];
        itemTemplate.setTag(dataIndex);
        this.onSetupItemData(itemTemplate, this._array, dataIndex);
    },

    jumpToItem: function (index) {
        if (this.direction == ccui.ScrollView.DIR_VERTICAL) {
            let offset = index * (this._itemTemplateHeight + this._spacing);
            if (this.listView.getInnerContainer().height - offset < this.listView.height)
                offset = this.listView.getInnerContainer().height - this.listView.height;
            this.listView.getInnerContainer().setPositionY(-offset);
        } else if (this.direction == ccui.ScrollView.DIR_HORIZONTAL) {
            //index乘以单个item的偏移量获得index的绝对偏移量
            let offset = index * (this._itemTemplateWidth + this._spacing);
            // 剩余内容小于偏移值时,按剩余内容计算
            if (this.listView.getInnerContainer().width - offset < this.listView.width)
                offset = this.listView.getInnerContainer().width - this.listView.width;

            // positionX为0listview展示最左侧的内容, 相当于index=0, positionX为this.listView.getInnerContainer().width - this.listView.width时,展示到列表内容的最后面
            this.listView.getInnerContainer().setPositionX(-offset);
        }
    },

    /**
     * 类似于前后翻页的效果
     * @param isBackward 是否往回翻页
     */
    jumpToAdjacent: function (isBackward) {
        this.listView.stopAutoScroll();
        if (this.direction == ccui.ScrollView.DIR_VERTICAL) {
            let offset = this.listView.height + this._spacing;


            //上下超边界判断
            if(isBackward){ // getPositionY的值增加,不能超过0
                if (this.listView.getInnerContainer().getPositionY() + offset >= 0){
                    this.listView.getInnerContainer().setPositionY(0);
                }else {
                    this.listView.getInnerContainer().setPositionY(this.listView.getInnerContainer().getPositionY() + offset);
                }
            }else { // getPositionX的值减少,不能低于 -this.listView.getInnerContainer().width
                if (this.listView.getInnerContainer().getPositionY() - offset <= -this.listView.getInnerContainer().height + this.listView.height){
                    this.listView.getInnerContainer().getPositionY(-this.listView.getInnerContainer().height + this.listView.height);
                }else {
                    this.listView.getInnerContainer().getPositionY(this.listView.getInnerContainer().getPositionY() - offset);
                }
            }
        } else if (this.direction == ccui.ScrollView.DIR_HORIZONTAL) {
            let offset = this.listView.width + this._spacing;
            //左右超边界判断
            if(isBackward){ // getPositionX的值增加,不能超过0
                if (this.listView.getInnerContainer().getPositionX() + offset >= 0){
                    this.listView.getInnerContainer().setPositionX(0);
                }else {
                    this.listView.getInnerContainer().setPositionX(this.listView.getInnerContainer().getPositionX() + offset);
                }
            }else { // getPositionX的值减少,不能低于 -this.listView.getInnerContainer().width
                if (this.listView.getInnerContainer().getPositionX() - offset <= -this.listView.getInnerContainer().width + this.listView.width){
                    this.listView.getInnerContainer().setPositionX(-this.listView.getInnerContainer().width + this.listView.width);
                }else {
                    this.listView.getInnerContainer().setPositionX(this.listView.getInnerContainer().getPositionX() - offset);
                }
            }
        }
    },


    onExit: function () {
        this._super();
        // 解决listView onExit再次onEnter时,layout数据被自行修改的问题
        this.saveListViewLayoutInfo();
        this._isReEnter = true;
    },

    onEnter: function () {
        this._super();
        // 解决listView onExit再次onEnter时,layout数据被自行修改的问题
        if (this._isReEnter) {
            setTimeout(function () {
                this.restoreListViewLayoutInfo();
                this._isReEnter = false;
            }.bind(this), 300);
        }
    },

    saveListViewLayoutInfo: function () {
        this._listViewLayoutInfo = [];
        let items = this.listView.getItems();
        for (let i = 0; i < items.length; i++) {
            this._listViewLayoutInfo.push(this.direction === ccui.ScrollView.DIR_HORIZONTAL ? items[i].getPositionX() : items[i].getPositionY());
        }

        this._listViewInnerContainerLastPosition = this.direction === ccui.ScrollView.DIR_HORIZONTAL ?
            this.listView.getInnerContainer().getPositionX() : this.listView.getInnerContainer().getPositionY();
    },

    restoreListViewLayoutInfo: function () {

        if(!cc.sys.isObjectValid(this.listView) || this._listViewLayoutInfo.length === 0)
            return;

        let isHorizontal = this.direction === ccui.ScrollView.DIR_HORIZONTAL;
        if(isHorizontal){
            this.listView.getInnerContainer().setPositionX(this._listViewInnerContainerLastPosition);
        }else {
            this.listView.getInnerContainer().setPositionY(this._listViewInnerContainerLastPosition);
        }

        let items = this.listView.getItems();
        for (let i = 0; i < items.length; i++) {
            if (isHorizontal) {
                items[i].setPositionX(this._listViewLayoutInfo[i]);
            } else {
                items[i].setPositionY(this._listViewLayoutInfo[i]);
            }
        }
    },

    selectedItemEvent: function (sender, type) {
        switch (type) {
            case ccui.ListView.ON_SELECTED_ITEM_END:
                let item = sender.getItem(sender.getCurSelectedIndex());
                cc.log("select child index = " + item.getTag());
                if (this.onItemClickCallback) {
                    this.onItemClickCallback(this._array[item.getTag()], item.getTag());
                }
                break;
            default:
                break;
        }
    }
});

新的数据列表只需要继承BaseListLayer并实现抛异常的方法即可
举个例子

var SearchResultListLayer = BaseListLayer.extend({
    setupListView: function (listView) {
        listView.setScrollBarWidth(10 * 2);
        listView.setScrollBarColor(cc.hexToColor("#813C0E"));
        listView.setScrollBarOpacity(255);
        // listView.setBackGroundImage(res.goBack);
        // listView.setBackGroundImageScale9Enabled(true);
        listView.setContentSize(cc.size(618 * 2, 288 * 2));
    },

    setupItemModel: function (defaultItem) {
        defaultItem.setContentSize(cc.size(618 * 2, this.listView.getContentSize().height));
        defaultItem.width = 618 * 2;
        defaultItem.height = 83 * 2;

        //字
        let words = new ccui.Text("", GC.font2, 46 * 2);
        words.setColor(cc.hexToColor("#813C0E"));
        words.string = "";
        words.setName("words");
        words.attr({
            anchorX: 0,
            anchorY: 0,
            x: 49 * 2,
            y: 16 * 2
        });
        defaultItem.addChild(words);

        let hBox = new ccui.HBox();
        hBox.setName("hBox");
        hBox.attr({
            /*anchorX: 0,
            anchorY: 0,*/
            x: 115 * 2,
            y: 70 * 2
        });
        let parameter = new ccui.LinearLayoutParameter();
        hBox.setLayoutParameter(parameter);

        let lp = new ccui.LinearLayoutParameter();
        lp.setMargin({left: 20 * 2, top: 0, right: 0, bottom: 0});

        //拼音
        let pinyin = new ccui.Text("", GC.font2, 18 * 2);
        pinyin.setColor(cc.hexToColor("#666666"));
        pinyin.string = "chuang";
        pinyin.setName("pinyin");
        pinyin.attr({
            anchorX: 0,
            anchorY: 0,
            x: 115 * 2,
            y: 42 * 2
        });
        defaultItem.addChild(pinyin);

        //课本名称+课文名称
        let bookAndLesson = new ccui.Text("", GC.font2, 18 * 2);
        bookAndLesson.setColor(cc.hexToColor("#666666"));
        bookAndLesson.string = "";
        bookAndLesson.setName("bookAndLesson");
        bookAndLesson.attr({
            anchorX: 0,
            anchorY: 0,
            x: 115 * 2,
            y: 17 * 2
        });
        //bookAndLesson.setContentSize(cc.size(340*2, 21*2));
        defaultItem.addChild(bookAndLesson);

        //学一学按钮
        var learnBtn = new ccui.Button();
        learnBtn.setName("learnBtn");
        learnBtn.setTitleText("学一学");
        learnBtn.setTitleFontName(GC.font2);
        learnBtn.setTitleFontSize(16 * 2);
        learnBtn.setTouchEnabled(true);
        learnBtn.loadTextures(res.yellowBtn, res.yellowBtn);
        learnBtn.attr({
            anchorY: 0,
            anchorX: 0,
            x: (618 - 117) * 2,
            y: 24 * 2
        });
        defaultItem.addChild(learnBtn);

        //虚线
        let dividerLine = new ccui.ImageView(res.dividerLine);
        dividerLine.attr({
            anchorY: 0,
            anchorX: 0,
            x: 38 * 2,
            y: 0,
        });
        dividerLine.width = 570 * 2;
        dividerLine.height = 2;
        defaultItem.addChild(dividerLine);
    },

    onSetupItemData:function(itemLayout, dataArray, index){
        let data = dataArray[index];
        itemLayout.getChildByName('words').setString(data['words']);
        itemLayout.getChildByName('pinyin').setString(data['pinyin'] + "   " + "部首:" + data['radical'] + "   " + "笔画:" + data['strokes']);
        //itemLayout.getChildByName('bookAndLesson').setString(data['category_name'] + data['book_name'] + " " + data['lesson_name']);
        TextUtil.setTextWithMaxWidth(itemLayout.getChildByName('bookAndLesson'), 340 * 2, data['book_name'] + data['category_name'] + " " + data['lesson_name'])
    }
});

图片压缩

图片压缩用的是 TexturePackerGUI,Windows上使用压缩后会有红图出现,根本没法用,苹果电脑上不会有这个情况,这是软件方故意做的免费软件限制,难不成是歧视window系统吗?不知道。


image.png

讨厌的红图


image.png

c层对象引用无效(Invalid Native Object)

E:\workspace\shuxiaotong_app\frameworks\runtime-src\proj.android\app\src\main\cocosAssets\script\jsb_property_impls.js:53:Error: js_cocos2dx_Node_getContentSize : Invalid Native Object

此问题一般都指向了jsb_property_impls.js没有具体的报错位置,为了找到这个位置,主要原因是因为某些原因导致js操作的c层对象被释放掉了,最常见的就是执行ajax网络请求后在返回结果中回调callback中的ui对象,但是这个时候很可能用户已经离开了当前页面,这种问题的解决办法就是在用户离开页面时,一定要及时取消网络调用并取消网络回调,具体问题具体分解即可。如果想知道具体的出错位置,我想到的解决办法是在关键位置打印方法调用栈,已经实现了,但是考虑到性能问题,也只有是在调试的时候用了几次,后来代码删除了,现在具体的代码插入点一时找不到了,大概是在cocos引擎的某个js文件里,有想要这个功能的朋友可以自己去实现一下。

不太确定是不是这里,jsb_property_apis.js,当时的想法应该是通过关键词缩问题查找范围

cc.Node.prototype.attr = function(attrs) {
    //cc.log("====== this: \n" + JSON.stringify(this) + "attrs " + JSON.stringify(attrs));
    for(var key in attrs) {
        // cc.log("====== this key " + key);
        // cc.log("====== attrs[key] " + attrs[key]);
        this[key] = attrs[key];
    }
    // cc.log("====== this end \n");
};

js调用原生函数并返回结果

项目中是由android原生部分和cocos部分组成的,也涉及到了很多数据调用问题,
cocos2d-x-js中本身封装了这样的api JSB(javascript binding)支持从 JS 端直接调用 Native 端(Android、iOS)
项目做了中间层封装,方便统一调用

if (cc.sys.os === cc.sys.OS_ANDROID) {
    /**
     * Toast提示
     */
    NativeMethod.showToast = function (msg) {
        if (msg != null && msg != '') {
            jsb.reflection.callStaticMethod(ANDROID_CLASS_NAME, "showToast", "(Ljava/lang/String;)V", msg);
        }
    };
} else if (cc.sys.os === cc.sys.OS_IOS) {

    NativeMethod.showToast = function (msg) {
        if (msg != null && msg != '') {
            jsb.reflection.callStaticMethod(IOS_CLASS_NAME, "toast:", msg);
        }
    };

另外还实现了一个管理从cocos到原生的调用并返回一个异步结果的管理类

/**
 * 用于管理从游戏到原生的调用并返回一个异步的结果
 * @type {{}}
 */
var CallbackManager = CallbackManager || {
    callbackMap:{},
};

CallbackManager.callCallback = function (callbackId, resultData, autoRemove = true) {
    //not a function 就 return
    let hasCallback = !SXTCommonUtils.isEmpty(this.callbackMap[callbackId]);
    if (!hasCallback){
        return;
    }

    if (resultData == '' || resultData == null){
        this.callbackMap[callbackId](null);
    } else {
        let isJson = VerifyUtil.isJSON(resultData);
        //如果是json返回json 不是的话正常返回
        if (isJson){
            let jsonData = JSON.parse(resultData);
            this.callbackMap[callbackId](jsonData);

        } else {
            this.callbackMap[callbackId](resultData);
        }
    }

    autoRemove && this.removeCallback(callbackId)
};
CallbackManager.addCallback = function(callback) {
    let timestamp = (new Date()).valueOf().toString();
    this.callbackMap[timestamp] = callback;
    return timestamp;
};
CallbackManager.removeCallback = function (callbackId) {
    delete this.callbackMap[callbackId]
};

用法就是在js层调用原生层时给CallbackManager添加callback并返回一个callbackId在通过JSB调用原生方法时带上callbackId,当原生方法异步执行完毕后,通过cocos提供的API Cocos2dxJavascriptJavaBridge.evalString回调CallbackManager并带着callbackId参数

NativeMethod.getUserInfo = function (callback) {
        let callbackId = CallbackManager.addCallback(callback);
        jsb.reflection.callStaticMethod(ANDROID_CLASS_NAME, "getUserInfo", "(Ljava/lang/String;)V", callbackId);
    };
Cocos2dxJavascriptJavaBridge.evalString("CallbackManager.callCallback('" + callbackId + "','" + userInfojson + "')");

cocos2d-x-js API新增功能接口

项目中用到了很多原生的功能,但是引擎没有实现只能自己动手,比如说这里


image.png
image.png

需要根据具体的业务需要进行修改

androidX迁移

androidX迁移遇到的最大的问题就是cocos资源打包脚本出现了问题,解决方法是新建了一个cocosAssets,将所有资源文件和脚本拷贝到这个资源文件夹里进行操作,这样一来还有个好处就是compileJS时不会影响到原生js文件
全部打包脚本如下

import org.gradle.internal.os.OperatingSystem

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
apply from: 'multiple-channel.gradle'
apply plugin: 'bugly'

android {
    useLibrary 'org.apache.http.legacy'
    compileSdkVersion buildVersions.compileSdkVersion

    defaultConfig {
        applicationId buildVersions.applicationId
        minSdkVersion buildVersions.minSdkVersion
        targetSdkVersion buildVersions.targetSdkVersion
        resConfigs "zh" //-280KB 删除其他语言资源,只支持中文
        vectorDrawables.useSupportLibrary = true
        manifestPlaceholders = [
                huawei_app_id: "100816393",
                vivo_api_key : "dfaddfb9-d97a-4e23-9c4a-39969822167c",
                vivo_app_id  : "15286"
        ]
        multiDexEnabled true
        versionCode buildVersions.versionCode
        versionName buildVersions.versionName

        if (buildVersions.ndk_build) { // 为了加快编译速度,这里做了ndk编译开关,当项目编译成功后,将编译产生的so提取出来,这样在引擎代码不发生改变时就不需要每次编译,可节约大量的编译时间。
            externalNativeBuild {
                if (PROP_BUILD_TYPE == 'ndk-build') {
                    ndkBuild {
                        targets 'cocos2djs'
                        arguments 'NDK_TOOLCHAIN_VERSION=clang'
                        arguments '-j' + Runtime.runtime.availableProcessors()
                    }
                } else if (PROP_BUILD_TYPE == 'cmake') {
                    cmake {
                        arguments "-DCMAKE_FIND_ROOT_PATH=", "-DANDROID_STL=c++_static", "-DANDROID_TOOLCHAIN=clang", "-DANDROID_ARM_NEON=TRUE", "-DUSE_CHIPMUNK=TRUE", "-DUSE_BULLET=TRUE", "-DBUILD_JS_LIBS=TRUE"
                        cppFlags "-frtti -fexceptions"
                        // prebuilt root must be defined as a directory which you have right to access or create if you use prebuilt
                        // set "-DGEN_COCOS_PREBUILT=ON" and "-DUSE_COCOS_PREBUILT=OFF" to generate prebuilt,  this way build cocos2d-x libs
                        // set "-DGEN_COCOS_PREBUILT=OFF" and "-DUSE_COCOS_PREBUILT=ON" to use prebuilt, this way not build cocos2d-x libs
                        //arguments "-DCOCOS_PREBUILT_ROOT=/Users/laptop/cocos-prebuilt"
                        //arguments "-DGEN_COCOS_PREBUILT=OFF", "-DUSE_COCOS_PREBUILT=OFF"
                    }
                }
            }
        }

        ndk {
            abiFilters = []
            abiFilters.addAll(PROP_APP_ABI.split(':').collect { it as String })
        }

        kapt {
            arguments {
                arg("moduleName", project.getName())
            }
        }
    }

    sourceSets.main {

        def dirs = ['main', 'm_user', 'm_patriarch_center', 'm_membership', 'm_browser', 'm_statistics', 'm_zxing', 'm_push']
        dirs.each { dir ->
            if (dir == 'main') {
                assets.srcDirs("src/main/assets", 'src/main/cocosAssets')
            } else {
                assets.srcDir("src/$dir/assets")
            }
            res.srcDir("src/$dir/res")
            java.srcDir("src/$dir/java")
            jniLibs.srcDir("src/$dir/libs")
        }
        assets.srcDir "assets"
        jniLibs.srcDir "libs"
        manifest.srcFile "AndroidManifest.xml"
    }
    if (buildVersions.ndk_build) {
        externalNativeBuild {
            if (PROP_BUILD_TYPE == 'ndk-build') {
                ndkBuild {
                    path "jni/Android.mk"
                }
            } else if (PROP_BUILD_TYPE == 'cmake') {
                cmake {
                    path "../../../../CMakeLists.txt"
                }
            }
        }
    }

    signingConfigs {
        release {
            keyAlias buildVersions.KEY_ALIAS
            keyPassword buildVersions.KEY_PASSWORD
            storeFile file(buildVersions.STORE_FILE)
            storePassword buildVersions.KEYSTORE_PASSWORD
        }
    }

    buildTypes {
        release {
            multiDexKeepProguard file('multidex-config.pro')
            debuggable false
            jniDebuggable false
            renderscriptDebuggable false
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
            ndk {
                abiFilters "armeabi-v7a"
            }
            externalNativeBuild {
                ndkBuild {
                    arguments 'NDK_DEBUG=0'
                }
            }
            manifestPlaceholders = [umengkey: '5c919fa03fc195a59000047a']
        }

        debug {
            debuggable true
            jniDebuggable true
            renderscriptDebuggable true
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
            externalNativeBuild {
                ndkBuild {
                    arguments 'NDK_DEBUG=1'
                }
            }
        }
    }

    dataBinding {
        enabled = true
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    lintOptions {
        lintConfig rootProject.file('lint.xml')
        abortOnError false
    }

}

def getCocosCommandPath() {
    if (OperatingSystem.current().isWindows()) {
        return 'cocos.bat'
    } else {
        // on unix like system, can not get environments variables easily
        // so run a shell script to get environment variable sets by cocos2d-x setup.py
//        new ByteArrayOutputStream().withStream { os ->
//            def result = exec {
//                executable = project.file('get_environment.sh')
//                println("=========================>>> executable $executable")
//                standardOutput = os
//            }
//            println("=========================>>> os.toString().trim() ${os.toString().trim()}")
//            ext.console_path = os.toString().trim()
//        }
        return '/usr/local/tools/cocos2d-console/bin/cocos'
    }
}

project.afterEvaluate {
    println("======> afterEvaluate start")

    Task copyFilesToAssets = project.task("copyFilesToAssets"){
        doFirst {
            println("======> copyFilesToAssets doFirst delete cocosAssets")
            def dir = "${rootDir}/app/src/main/cocosAssets"
            delete dir
        }

        doLast{
            println("======> processAssetFileTask copy cocos res files")
            copy {
                from "${buildDir}/../../../../../res"
                into "${rootDir}/app/src/main/cocosAssets/res"
            }

            println("======> processAssetFileTask copy cocos src files")
            copy {
                from "${buildDir}/../../../../../src"
                into "${rootDir}/app/src/main/cocosAssets/src"
            }

            println("======> processAssetFileTask copy cocos scripts")
            copy {
                from "${buildDir}/../../../../cocos2d-x/cocos/scripting/js-bindings/script"
                into "${rootDir}/app/src/main/cocosAssets/script"
            }

            println("======> processAssetFileTask copy cocos main file")
            copy {
                from "${buildDir}/../../../../../main.js"
                from "${buildDir}/../../../../../project.json"
                into "${rootDir}/app/src/main/cocosAssets"
            }
        }
    }

    // a method used to invoke the cocos jscompile command
    Task compileJS = project.task("compileJS"){
        doFirst {
            println("======> compileJS")
            def dir = "${rootDir}/app/src/main/cocosAssets"
            def compileArgs = ['jscompile', '-s', dir, '-d', dir]

            println 'running command : ' + 'cocos ' + compileArgs.join(' ')
            exec {
                // if you meet problem, just replace `getCocosCommandPath()` to the path of cocos command
                executable getCocosCommandPath()
                args compileArgs
            }
        }
    }

    Task deleteJsFiles = project.task("deleteJsFiles"){
        doFirst {
            println("======> deleteJsFiles")
            def dir = "${rootDir}/app/src/main/cocosAssets"
            // remove the js files in dstDir
            delete fileTree(dir) {
                include '**/*.js'
                //include '**/jssupport/*.jsc'
                //exclude '**/jssupport/*.js'
            }
        }
    }

    tasks.findByName('mergeDebugAssets')?.dependsOn copyFilesToAssets

    compileJS.dependsOn copyFilesToAssets
    deleteJsFiles.dependsOn compileJS
    tasks.findByName('mergeReleaseAssets')?.dependsOn deleteJsFiles
}

android.applicationVariants.all { variant ->
    if (buildVersions.ndk_build) delete "${project.file('libs/armeabi-v7a/libcocos2djs.so')}"

    String suffix = variant.variantData.name.capitalize()
    Task mergeAssetsTask = tasks.findByName("merge${suffix}Assets")

    mergeAssetsTask.doLast {
        println("======> mergeAssetsTask doLast start")
        // compile the scripts if necessary
        def compileScript = (variant.name.compareTo('release') == 0)
        if (project.hasProperty('PROP_COMPILE_SCRIPT')) {
            compileScript = (PROP_COMPILE_SCRIPT.compareTo('1') == 0)
        }
        println("======> mergeAssetsTask doLast finished")
    }
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation project(':libcocos2dx')
    implementation deps.kotlin.stdlib
    implementation deps.support.app_compat
    implementation deps.support.recyclerview
    implementation deps.support.design
    implementation deps.support.v4
    implementation deps.constraint_layout
    implementation deps.arouter.api
    implementation deps.autodispose.autodispose
    implementation deps.autodispose.autodispose_android_archcomponents
    implementation deps.retrofit.runtime
    implementation deps.retrofit.gson
    implementation deps.anko.common
    implementation deps.anko.v4_commons
    implementation deps.slidingtab
    implementation deps.paging
    implementation deps.bannerview
    implementation deps.refresh
    implementation deps.logger
    implementation deps.biding_recycler_view
    implementation deps.analytics
    implementation deps.walle
    implementation deps.refresh
    implementation deps.status_bar_compat
    implementation(deps.permissionsdispatcher.api) {
        exclude group: 'com.android.support'
    }
    implementation deps.support.v4
    implementation deps.constraint_layout
    implementation deps.ucrop
    implementation deps.easyimage
    implementation deps.lifecycle.extensions
    implementation deps.zxing
    kapt deps.room.compiler
    kapt deps.lifecycle.compiler
    kapt deps.arouter.compiler
    kapt deps.permissionsdispatcher.compiler

    implementation project(':netlib')
    implementation project(':resouce')
    implementation project(':support')
    implementation project(':widget')
    implementation project(':router')
    implementation project(':social')
    implementation project(':speechevaluator')
    implementation project(':pay')
    implementation deps.android_pickerview

    implementation deps.eventbus
    implementation deps.support.vector
    implementation deps.immersionbar
    implementation deps.countdownview

    implementation deps.push
}

上一篇下一篇

猜你喜欢

热点阅读