You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
AdLike2048/assets/mx/components/SpriteLoad.ts

335 lines
11 KiB

3 weeks ago
//************************
// Create By 流云
// Time: 2025.01.15
// Desc: 设置图片资源组件
//************************
import { size, Size, Texture2D, UITransform, Vec2 } from 'cc';
import { AssetManager } from 'cc';
import { ImageAsset } from 'cc';
import { _decorator, assetManager, Component, Node, Sprite, SpriteFrame } from 'cc';
import MTools from '../tools/MTools';
import { BundleLifecycle } from '../module/res/BundleLifecycle';
const { ccclass, property, executeInEditMode } = _decorator;
/**
* 设置图片资源组件
*/
@ccclass('SpriteLoad')
//@executeInEditMode
export class SpriteLoad extends Component {
@property
private bundleName: string = "icons";
@property({ type: Node })
private SpriteNode: Node = null;
@property
private AdaptSize: boolean = false;
spComp: Sprite = null;
path: string = "";
private _loadedRes = null;
private _initSize: Size = null;
private _curentPath = "";
/** 本地/Bundle 加载世代:节点回池或再次 setSprite 时递增,作废过期异步回调 */
private _localGen = 0;
/** 远程加载世代,错开列表复用时的过期回调 */
private _remoteGen = 0;
/** 当前远程加载结束回调 */
private _remoteCb: ((ok: boolean) => void) | null = null;
/** 远程图地址;onEnable 按此重拉 */
private _remoteUrl = '';
onLoad() {
this._initSize = this.node.getComponent(UITransform).contentSize.clone();
if (this.SpriteNode) {
this.spComp = this.SpriteNode.getComponent(Sprite);
} else {
this.SpriteNode = this.node;
this.spComp = this.getComponent(Sprite);
}
}
protected onEnable(): void {
if (this.spComp?.isValid) this.spComp.enabled = true;
if (this._remoteUrl) {
this._startRemote(this._remoteUrl, null);
return;
}
if (this.path != "") {
this.loadSprite();
}
}
protected onDisable(): void {
// 临时隐藏只释放引用,不清 path,否则再显示时 onEnable 无法重载
this._localGen++;
this.releaseLoaded();
}
protected onDestroy(): void {
this._localGen++;
this.clear();
}
/**
* 释放当前已加载 SpriteFrame,保留 path。
* 须先摘掉 Sprite 引用再 decRef:否则最后一引用销毁后 uv 已空,Batcher 仍 updateUVs 会读 null[0]。
*/
private releaseLoaded() {
const sp = this.spComp;
if (sp?.isValid) {
sp.enabled = false;
if (sp.spriteFrame) sp.spriteFrame = null;
}
if (this._loadedRes) {
this._loadedRes.decRef();
this._loadedRes = null;
}
}
/**
* 清除图片(含 path,用于销毁或主动清空)
*/
clear() {
this.releaseLoaded();
this._curentPath = "";
this.path = "";
this._remoteUrl = "";
this._remoteGen++;
this._remoteCb = null;
}
/**
* 设置图片
* @param path 图片路径(传空则清除当前的图片)
* @param bundleName 资源包名
* @returns
*/
setSprite(path: string, bundleName: string = "", callback = null) {
this._remoteUrl = '';
this._remoteGen++;
this._localGen++;
this.ensureSprite();
if (this.spComp == null) {
console.error('[SpriteLoad] setSprite 缺少 Sprite', this.node?.name, path);
return;
}
if (this._initSize == null) {
this._initSize = this.node.getComponent(UITransform).contentSize.clone();
}
if (bundleName != "") {
this.bundleName = bundleName;
}
this.path = path;
let name = path.split("/").pop();
if (this.bundleName == "" || this.path == "") {
return;
}
// this.spComp.spriteFrame = gg.res.getRes(name, this.path, this.bundleName, SpriteFrame);
// return;
this.path += "/spriteFrame";
if (this._curentPath != "" && this._curentPath == this.path && this._loadedRes && this.spComp.spriteFrame) return;
//this.clear();
// this.scheduleOnce(() => {
// this.loadSprite(callback);
// }, 0.1);
this.loadSprite(callback);
}
/**
* 加载远程图片
* @param url 远程 url
* @param callback 结束回调,成功 true
*/
loadRemote(url: string, callback: ((ok: boolean) => void) | null = null): void {
this.path = '';
this._curentPath = '';
this._remoteUrl = url;
this._startRemote(url, callback);
}
/**
* 发起远程加载(不改 _remoteUrl,供 onEnable 重拉)。
* @param url 地址
* @param callback 结束回调
*/
private _startRemote(url: string, callback: ((ok: boolean) => void) | null): void {
this.ensureSprite();
if (this.spComp == null) {
console.error('[SpriteLoad] loadRemote 缺少 Sprite', this.node?.name, url);
if (callback) callback(false);
return;
}
this._remoteCb = callback;
const gen = ++this._remoteGen;
if (this.hasImageExtension(url)) {
this.loadTexture(url, gen);
} else {
this.loadTextureExt(url, gen);
}
}
/** 父节点未激活时 onLoad 未跑,补齐 Sprite */
private ensureSprite(): void {
if (this.spComp != null) return;
if (this.SpriteNode) {
this.spComp = this.SpriteNode.getComponent(Sprite);
} else {
this.SpriteNode = this.node;
this.spComp = this.getComponent(Sprite);
}
}
private loadSprite(callback = null) {
const gen = ++this._localGen;
this.loadBundle((bundle: AssetManager.Bundle | null) => {
if (gen !== this._localGen || !this.isValid) {
if (callback) callback(false);
return;
}
if (!bundle) {
console.error('[SpriteLoad] bundle 不可用', this.bundleName, this.path);
if (callback) callback(false);
return;
}
let res = bundle.get(this.path, SpriteFrame);
if (res) {
if (!res.isValid) {
console.error('[SpriteLoad] 缓存 SpriteFrame 已失效', this.bundleName, this.path);
if (callback) callback(false);
return;
}
if (this.node && this.node.isValid && this.spComp) {
if (this._loadedRes === res && this.spComp.spriteFrame === res) {
this.adoptSize();
if (callback) callback(true);
return;
}
this.releaseLoaded();
this.spComp.enabled = true;
this.spComp.spriteFrame = res;
this._curentPath = this.path;
this._loadedRes = res;
res.addRef();
}
this.adoptSize();
if (callback) callback(true);
return;
}
bundle.load(this.path, SpriteFrame, (err, sp) => {
// 回池/再次 setSprite 后的过期回调不得写回,否则 decRef 后 Batcher 仍读到空纹理
if (gen !== this._localGen || !this.isValid) {
if (callback) callback(false);
return;
}
if (err || !sp) {
console.error('[SpriteLoad] sprite 加载失败', this.bundleName, this.path, String(err));
if (callback) callback(false);
return;
}
if (this.node && this.node.isValid && this.spComp) {
this.releaseLoaded();
this.spComp.enabled = true;
this.spComp.spriteFrame = sp;
this._curentPath = this.path;
this._loadedRes = sp;
this._loadedRes.addRef();
}
if (callback) {
callback(true);
}
this.adoptSize();
});
});
}
private loadBundle(callback: (bundle: AssetManager.Bundle | null) => void) {
if (!this.bundleName) {
console.error('[SpriteLoad] bundleName 为空', this.node?.name);
callback(null);
return;
}
void BundleLifecycle.getInstance().ensureLoaded(this.bundleName).then((bundle) => {
if (!bundle) {
console.error('[SpriteLoad] bundle 加载失败', this.bundleName);
}
callback(bundle);
});
}
private loadTexture(url: string, gen: number): void {
assetManager.loadRemote<ImageAsset>(url, (err, imageAsset) => {
this.applyRemote(gen, err, imageAsset, url);
});
}
/** 远程 url 无后缀时须指定类型 */
private loadTextureExt(url: string, gen: number, ext = '.png'): void {
assetManager.loadRemote<ImageAsset>(url, { ext }, (err, imageAsset) => {
this.applyRemote(gen, err, imageAsset, url);
});
}
/** 远程图回写 Sprite,忽略过期请求 */
private applyRemote(gen: number, err: Error | null, imageAsset: ImageAsset, url: string): void {
if (gen !== this._remoteGen) return;
const cb = this._remoteCb;
this._remoteCb = null;
if (err || !imageAsset) {
console.error('[SpriteLoad] 远程图加载失败', url, String(err));
if (cb) cb(false);
return;
}
if (!this.node?.isValid || !this.spComp?.isValid) {
console.error('[SpriteLoad] 远程图回调时节点已销毁', url);
if (cb) cb(false);
return;
}
this.releaseLoaded();
const spriteFrame = new SpriteFrame();
const texture = new Texture2D();
texture.image = imageAsset;
spriteFrame.texture = texture;
spriteFrame.addRef();
this._loadedRes = spriteFrame;
this.spComp.enabled = true;
this.spComp.spriteFrame = spriteFrame;
if (cb) cb(true);
}
/**
* 判断图片 url 路径是否带后缀(忽略 query)
* @param url 远程地址
*/
private hasImageExtension(url: string): boolean {
const path = url.split('?')[0].split('#')[0];
return /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(path);
}
private adoptSize() {
if (this.spComp == null) return;
if (this.spComp.spriteFrame == null) return;
let spriteFrame = this.spComp.spriteFrame;
let width = spriteFrame.originalSize.width;
let height = spriteFrame.originalSize.height;
if (this.AdaptSize) {
let s = MTools.adaptSize(this._initSize.width, this._initSize.height, width, height);
this.node.getComponent(UITransform).contentSize = size(s.x, s.y);
}
}
}