//************************ // 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(url, (err, imageAsset) => { this.applyRemote(gen, err, imageAsset, url); }); } /** 远程 url 无后缀时须指定类型 */ private loadTextureExt(url: string, gen: number, ext = '.png'): void { assetManager.loadRemote(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); } } }