cocos2d-x-js(cocos2d-x-3.17) 項(xiàng)目經(jīng)驗(yàn)總結(jié)(一)

背景及介紹

cocos2d-x-js的安裝和基本用法本文不做討論,主要總結(jié)一下于項(xiàng)目相關(guān)的經(jīng)驗(yàn)。
項(xiàng)目是一款針對(duì)學(xué)齡前和小學(xué)生的漢語(yǔ)學(xué)習(xí)移動(dòng)端app(Android、iOS)自赔,里面會(huì)用到一些動(dòng)畫(huà)特效和粒子特殊,經(jīng)過(guò)一番調(diào)研最終采用了cocos2d-x-js (2019年4月到2021年4月)柳琢,經(jīng)過(guò)了前期的環(huán)境配置和安裝绍妨,我們來(lái)到了api學(xué)習(xí)和熟悉階段润脸,借助官方提供的“吃壽司”小demo以及官方文檔,我們快速的熟悉了cocos2d-x-js的一些基本用法他去,之后便來(lái)到了大量的需求實(shí)現(xiàn)階段津函,下面是在需求實(shí)現(xiàn)中多次試錯(cuò)后取得的成果。

frameworks代碼已上傳到github https://github.com/kevin-mob/cocos2djs

屏幕適配

設(shè)計(jì)圖以iPhone6的屏幕尺寸為標(biāo)準(zhǔn)進(jìn)行設(shè)計(jì)孤页,并通過(guò)動(dòng)態(tài)計(jì)算縮放寬高最終達(dá)到全屏尺寸并去除黑邊的效果。

相關(guān)計(jì)算代碼

    // 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適配保證內(nèi)容完全展示涩馆,對(duì)(寬或高的)黑邊區(qū)域進(jìn)行縮放處理
    // 當(dāng)前屏幕寬高比小于設(shè)計(jì)分辨率(1334*750)行施,例如設(shè)備iPad,會(huì)出現(xiàn)上下黑邊魂那,需要對(duì)高度進(jìn)行放大到全屏處理蛾号。
    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 {
        //當(dāng)前屏幕寬高比大于設(shè)計(jì)分辨率(1334*750),例如設(shè)備iPhoneX涯雅,會(huì)出現(xiàn)左右黑邊鲜结,需要對(duì)度進(jìn)行放大到全屏處理。
        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適配保證內(nèi)容完全展示活逆,對(duì)(寬或高的)黑邊區(qū)域進(jìn)行縮放處理
    // 當(dāng)前屏幕寬高比小于設(shè)計(jì)分辨率(1334*750)精刷,例如設(shè)備iPad,會(huì)出現(xiàn)上下黑邊蔗候,需要對(duì)高度進(jìn)行放大到全屏處理怒允。
    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 {
        //當(dāng)前屏幕寬高比大于設(shè)計(jì)分辨率(1334*750),例如設(shè)備iPhoneX锈遥,會(huì)出現(xiàn)左右黑邊纫事,需要對(duì)度進(jìn)行放大到全屏處理。
        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復(fù)用

cocos2d-x-js(cocos2d-x-3.17)的demo中提供了一個(gè)用于展示列表數(shù)據(jù)的demo位置在GUITest>UIListViewTest中所灸,簡(jiǎn)單少量的數(shù)據(jù)展示可以使用丽惶,但是數(shù)據(jù)量一旦變多在使用的話會(huì)出現(xiàn)大量占用內(nèi)存導(dǎo)致的卡頓甚至是閃退,原因是demo中并沒(méi)有ui對(duì)象復(fù)用的邏輯爬立,經(jīng)過(guò)了一番邏輯梳理钾唬,結(jié)合android中ListView的繪制原理,對(duì)該組件做了大量的優(yōu)化改進(jìn)工作懦尝,最終解決了卡頓知纷、閃退的問(wèn)題。
全部代碼

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 復(fù)用的偏移量為 原始_spawnCount 個(gè)view的寬度之和陵霉,可以改為根據(jù)ListView寬度自動(dòng)計(jì)算復(fù)用寬度和_spawnCount琅轧,無(wú)需外部指定_spawnCount個(gè)數(shù)
            this._reuseItemOffset = (this._itemTemplateWidth + this._spacing) * this._spawnCount;
        }
    },

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

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

    /**
     * 進(jìn)行itemLayout和數(shù)據(jù)綁定操作
     * @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++) {
            // 超過(guò)_spawnCount數(shù)量的數(shù)據(jù)后停止預(yù)渲染
            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));
                //更新數(shù)據(jù) 移動(dòng)內(nèi)容到最前面
                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));
                //更新數(shù)據(jù) 移動(dòng)內(nèi)容到最前面
                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;

        //手勢(shì)的滑動(dòng)方向
        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();

        // 屏幕在內(nèi)容上的移動(dòng)方向
        var isRight = this.listView.getInnerContainer().getPosition().x < this._lastContentPosX;
        // jumpToItem時(shí),計(jì)算幾倍重用
        var moveMultiple = Math.abs((this.listView.getInnerContainer().getPosition().x - this._lastContentPosX) / this._reuseItemOffset);
        moveMultiple = Math.ceil(moveMultiple);

        // 緩沖區(qū)設(shè)為4個(gè)模板view的寬度
        this._bufferZone = this._itemTemplateWidth * 4;

        if (isRight) {
            if (moveMultiple > 1) {
                // 跳躍式更新時(shí)(單次刷新x移動(dòng)超過(guò)一屏)踊挠,先刷新目標(biāo)屏幕的前一屏數(shù)據(jù)乍桂,再刷新目標(biāo)屏數(shù)據(jù)冲杀,保證顯示沒(méi)有空白
                this._ascendUpdate(moveMultiple - 1, totalWidth, items);
                this._ascendUpdate(1, totalWidth, items);
            } else {
                this._ascendUpdate(moveMultiple, totalWidth, items);
            }
        } else {
            if (moveMultiple > 1) {
                // 跳躍式更新時(shí)(單次刷新x移動(dòng)超過(guò)一屏),先刷新目標(biāo)屏幕的前一屏數(shù)據(jù)睹酌,再刷新目標(biāo)屏數(shù)據(jù)权谁,保證顯示沒(méi)有空白
                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,復(fù)用左邊超出緩沖區(qū)的view
    _ascendUpdate: function (moveMultiple, totalWidth, items) {
        let dataIndex;
        let item;
        let itemPos;
        let itemCount = items.length;
        // 遍歷items找到緩沖區(qū)左邊的view進(jìn)行復(fù)用憋沿, 計(jì)算的最終PositionX超過(guò)右邊界停止更新旺芽,列表到頭了
        for (let i = 0; i < itemCount && i < this._totalCount; i++) {
            item = items[i];
            itemPos = this.getItemPositionXInView(item);
            //找到緩沖區(qū)外面的view進(jìn)行復(fù)用并且判斷是否超出了總區(qū)域的右邊界
            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,復(fù)用右邊超出緩沖區(qū)的view
    _descendUpdate: function (moveMultiple, totalWidth, items) {
        let dataIndex;
        let item;
        let itemPos;
        let itemCount = items.length;
        let listViewWidth = this.listView.getContentSize().width;
        // 遍歷items找到緩沖區(qū)右邊的view進(jìn)行復(fù)用辐啄, 計(jì)算的最終PositionX超過(guò)左邊界停止更新采章,列表到頭了
        for (let i = Math.min(itemCount, this._totalCount) - 1; i >= 0; i--) {
            item = items[i];
            itemPos = this.getItemPositionXInView(item);
            //找到緩沖區(qū)右邊的view進(jìn)行復(fù)用,并且判斷是否超出了總區(qū)域的左邊界
            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乘以單個(gè)item的偏移量獲得index的絕對(duì)偏移量
            let offset = index * (this._itemTemplateWidth + this._spacing);
            // 剩余內(nèi)容小于偏移值時(shí)壶辜,按剩余內(nèi)容計(jì)算
            if (this.listView.getInnerContainer().width - offset < this.listView.width)
                offset = this.listView.getInnerContainer().width - this.listView.width;

            // positionX為0listview展示最左側(cè)的內(nèi)容悯舟, 相當(dāng)于index=0, positionX為this.listView.getInnerContainer().width - this.listView.width時(shí)砸民,展示到列表內(nèi)容的最后面
            this.listView.getInnerContainer().setPositionX(-offset);
        }
    },

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


            //上下超邊界判斷
            if(isBackward){ // getPositionY的值增加抵怎,不能超過(guò)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的值增加岭参,不能超過(guò)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時(shí),layout數(shù)據(jù)被自行修改的問(wèn)題
        this.saveListViewLayoutInfo();
        this._isReEnter = true;
    },

    onEnter: function () {
        this._super();
        // 解決listView onExit再次onEnter時(shí)冗荸,layout數(shù)據(jù)被自行修改的問(wèn)題
        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;
        }
    }
});

新的數(shù)據(jù)列表只需要繼承BaseListLayer并實(shí)現(xiàn)拋異常的方法即可
舉個(gè)例子

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);

        //學(xué)一學(xué)按鈕
        var learnBtn = new ccui.Button();
        learnBtn.setName("learnBtn");
        learnBtn.setTitleText("學(xué)一學(xué)");
        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'] + "   " + "筆畫(huà):" + 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上使用壓縮后會(huì)有紅圖出現(xiàn),根本沒(méi)法用蚌本,蘋(píng)果電腦上不會(huì)有這個(gè)情況盔粹,這是軟件方故意做的免費(fèi)軟件限制,難不成是歧視window系統(tǒng)嗎程癌?不知道舷嗡。


image.png

討厭的紅圖


image.png

c層對(duì)象引用無(wú)效(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

此問(wèn)題一般都指向了jsb_property_impls.js沒(méi)有具體的報(bào)錯(cuò)位置,為了找到這個(gè)位置嵌莉,主要原因是因?yàn)槟承┰驅(qū)е耲s操作的c層對(duì)象被釋放掉了进萄,最常見(jiàn)的就是執(zhí)行ajax網(wǎng)絡(luò)請(qǐng)求后在返回結(jié)果中回調(diào)callback中的ui對(duì)象,但是這個(gè)時(shí)候很可能用戶已經(jīng)離開(kāi)了當(dāng)前頁(yè)面锐峭,這種問(wèn)題的解決辦法就是在用戶離開(kāi)頁(yè)面時(shí)中鼠,一定要及時(shí)取消網(wǎng)絡(luò)調(diào)用并取消網(wǎng)絡(luò)回調(diào),具體問(wèn)題具體分解即可沿癞。如果想知道具體的出錯(cuò)位置援雇,我想到的解決辦法是在關(guān)鍵位置打印方法調(diào)用棧,已經(jīng)實(shí)現(xiàn)了椎扬,但是考慮到性能問(wèn)題惫搏,也只有是在調(diào)試的時(shí)候用了幾次具温,后來(lái)代碼刪除了,現(xiàn)在具體的代碼插入點(diǎn)一時(shí)找不到了筐赔,大概是在cocos引擎的某個(gè)js文件里铣猩,有想要這個(gè)功能的朋友可以自己去實(shí)現(xiàn)一下。

不太確定是不是這里茴丰,jsb_property_apis.js达皿,當(dāng)時(shí)的想法應(yīng)該是通過(guò)關(guān)鍵詞縮問(wèn)題查找范圍

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調(diào)用原生函數(shù)并返回結(jié)果

項(xiàng)目中是由android原生部分和cocos部分組成的,也涉及到了很多數(shù)據(jù)調(diào)用問(wèn)題贿肩,
cocos2d-x-js中本身封裝了這樣的api JSB(javascript binding)支持從 JS 端直接調(diào)用 Native 端(Android鳞绕、iOS)
項(xiàng)目做了中間層封裝,方便統(tǒng)一調(diào)用

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);
        }
    };

另外還實(shí)現(xiàn)了一個(gè)管理從cocos到原生的調(diào)用并返回一個(gè)異步結(jié)果的管理類

/**
 * 用于管理從游戲到原生的調(diào)用并返回一個(gè)異步的結(jié)果
 * @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層調(diào)用原生層時(shí)給CallbackManager添加callback并返回一個(gè)callbackId在通過(guò)JSB調(diào)用原生方法時(shí)帶上callbackId尸曼,當(dāng)原生方法異步執(zhí)行完畢后,通過(guò)cocos提供的API Cocos2dxJavascriptJavaBridge.evalString回調(diào)CallbackManager并帶著callbackId參數(shù)

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新增功能接口

項(xiàng)目中用到了很多原生的功能萄焦,但是引擎沒(méi)有實(shí)現(xiàn)只能自己動(dòng)手控轿,比如說(shuō)這里


image.png

image.png

需要根據(jù)具體的業(yè)務(wù)需要進(jìn)行修改

androidX遷移

androidX遷移遇到的最大的問(wèn)題就是cocos資源打包腳本出現(xiàn)了問(wèn)題,解決方法是新建了一個(gè)cocosAssets拂封,將所有資源文件和腳本拷貝到這個(gè)資源文件夾里進(jìn)行操作茬射,這樣一來(lái)還有個(gè)好處就是compileJS時(shí)不會(huì)影響到原生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 刪除其他語(yǔ)言資源,只支持中文
        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編譯開(kāi)關(guān)在抛,當(dāng)項(xiàng)目編譯成功后,將編譯產(chǎn)生的so提取出來(lái)萧恕,這樣在引擎代碼不發(fā)生改變時(shí)就不需要每次編譯刚梭,可節(jié)約大量的編譯時(shí)間。
            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
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末票唆,一起剝皮案震驚了整個(gè)濱河市朴读,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌走趋,老刑警劉巖衅金,帶你破解...
    沈念sama閱讀 219,110評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異簿煌,居然都是意外死亡氮唯,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)姨伟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)惩琉,“玉大人,你說(shuō)我怎么就攤上這事授滓×账” “怎么了肆糕?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,474評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)在孝。 經(jīng)常有香客問(wèn)我诚啃,道長(zhǎng),這世上最難降的妖魔是什么私沮? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,881評(píng)論 1 295
  • 正文 為了忘掉前任始赎,我火速辦了婚禮,結(jié)果婚禮上仔燕,老公的妹妹穿的比我還像新娘造垛。我一直安慰自己,他們只是感情好晰搀,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布五辽。 她就那樣靜靜地躺著,像睡著了一般外恕。 火紅的嫁衣襯著肌膚如雪杆逗。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,698評(píng)論 1 305
  • 那天鳞疲,我揣著相機(jī)與錄音罪郊,去河邊找鬼。 笑死尚洽,一個(gè)胖子當(dāng)著我的面吹牛悔橄,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播腺毫,決...
    沈念sama閱讀 40,418評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼癣疟,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了潮酒?” 一聲冷哼從身側(cè)響起争舞,我...
    開(kāi)封第一講書(shū)人閱讀 39,332評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎澈灼,沒(méi)想到半個(gè)月后竞川,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,796評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡叁熔,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評(píng)論 3 337
  • 正文 我和宋清朗相戀三年委乌,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片荣回。...
    茶點(diǎn)故事閱讀 40,110評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡遭贸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出心软,到底是詐尸還是另有隱情壕吹,我是刑警寧澤著蛙,帶...
    沈念sama閱讀 35,792評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站耳贬,受9級(jí)特大地震影響踏堡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜咒劲,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評(píng)論 3 331
  • 文/蒙蒙 一顷蟆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧腐魂,春花似錦帐偎、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,003評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至兔毒,卻和暖如春嫉父,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背眼刃。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,130評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留摇肌,地道東北人擂红。 一個(gè)月前我還...
    沈念sama閱讀 48,348評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像围小,于是被迫代替她去往敵國(guó)和親昵骤。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容