背景及介紹
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)嗎程癌?不知道舷嗡。
討厭的紅圖
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ō)這里
需要根據(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
}