167 lines
7.1 KiB
C#
167 lines
7.1 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.U2D;
|
|
using UnityEngine.UI;
|
|
using YooAsset;
|
|
|
|
namespace HotUpdate.UI
|
|
{
|
|
/// <summary>移植 AtlasManager 的延迟绑定;所有平台异步加载,句柄随 UI 生命周期释放。</summary>
|
|
public sealed class UiAtlasLoader : MonoBehaviour
|
|
{
|
|
private sealed class Entry
|
|
{
|
|
public bool Loading = true;
|
|
public SpriteAtlas Atlas;
|
|
public AssetHandle Handle;
|
|
}
|
|
|
|
private readonly Dictionary<string, Entry> _atlases = new Dictionary<string, Entry>(StringComparer.Ordinal);
|
|
private readonly Dictionary<string, Sprite> _sprites = new Dictionary<string, Sprite>(StringComparer.Ordinal);
|
|
private readonly Dictionary<Image, object> _imageRequests = new Dictionary<Image, object>();
|
|
private ResourcePackage _preferredPackage;
|
|
private bool _allowEditorFallback = true;
|
|
private bool _disposed;
|
|
|
|
public void Init(ResourcePackage preferredPackage = null, bool allowEditorFallback = true)
|
|
{
|
|
_preferredPackage = preferredPackage;
|
|
_allowEditorFallback = allowEditorFallback;
|
|
}
|
|
|
|
private void Awake() { SpriteAtlasManager.atlasRequested += RequestAtlas; }
|
|
|
|
private void RequestAtlas(string atlasName, Action<SpriteAtlas> callback)
|
|
{
|
|
// 只接管本项目的 UI 图集,不拦截字体或第三方图集。
|
|
if (string.IsNullOrEmpty(atlasName) || !atlasName.StartsWith(UiAtlasPaths.AtlasNamePrefix, StringComparison.Ordinal)
|
|
|| !atlasName.EndsWith("Atlas", StringComparison.Ordinal)
|
|
|| atlasName.IndexOfAny(new[] { '/', '\\' }) >= 0) return;
|
|
StartCoroutine(LoadAtPathAsync(UiAtlasPaths.AtlasRoot + "/" + atlasName + ".spriteatlas", callback));
|
|
}
|
|
|
|
public IEnumerator LoadAtlasAsync(string relativeFolder, Action<SpriteAtlas> onLoaded)
|
|
{
|
|
return LoadAtPathAsync(UiAtlasPaths.GetAtlasPath(relativeFolder), onLoaded);
|
|
}
|
|
|
|
private IEnumerator LoadAtPathAsync(string path, Action<SpriteAtlas> onLoaded)
|
|
{
|
|
if (_disposed) { onLoaded?.Invoke(null); yield break; }
|
|
if (_atlases.TryGetValue(path, out var cached))
|
|
{
|
|
while (cached.Loading && !_disposed) yield return null;
|
|
if (!_disposed) onLoaded?.Invoke(cached.Atlas);
|
|
yield break;
|
|
}
|
|
var entry = new Entry();
|
|
_atlases.Add(path, entry);
|
|
var packages = new List<ResourcePackage>();
|
|
if (_preferredPackage != null) packages.Add(_preferredPackage);
|
|
if (YooAssets.IsInitialized)
|
|
foreach (string name in new[] { "Art", "DefaultPackage" })
|
|
if (YooAssets.TryGetPackage(name, out var package) && !packages.Contains(package)) packages.Add(package);
|
|
try
|
|
{
|
|
foreach (var package in packages)
|
|
{
|
|
if (package.InitializeStatus != EOperationStatus.Succeeded || !package.IsLocationValid(path)) continue;
|
|
entry.Handle = package.LoadAssetAsync<SpriteAtlas>(path);
|
|
yield return entry.Handle;
|
|
if (_disposed) yield break;
|
|
if (entry.Handle.Status == EOperationStatus.Succeeded)
|
|
{
|
|
entry.Atlas = entry.Handle.GetAssetObject<SpriteAtlas>();
|
|
if (entry.Atlas != null) break;
|
|
}
|
|
entry.Handle.Release();
|
|
entry.Handle = null;
|
|
}
|
|
#if UNITY_EDITOR
|
|
if (entry.Atlas == null && _allowEditorFallback)
|
|
entry.Atlas = UnityEditor.AssetDatabase.LoadAssetAtPath<SpriteAtlas>(path);
|
|
#endif
|
|
}
|
|
finally
|
|
{
|
|
entry.Loading = false;
|
|
// 失败不留缓存,允许资源包就绪后重试。
|
|
if (entry.Atlas == null && !_disposed)
|
|
{
|
|
entry.Handle?.Release();
|
|
entry.Handle = null;
|
|
_atlases.Remove(path);
|
|
}
|
|
}
|
|
if (_disposed) yield break;
|
|
if (entry.Atlas == null) Debug.LogWarning("[UIAtlas] Atlas unavailable: " + path);
|
|
onLoaded?.Invoke(entry.Atlas);
|
|
}
|
|
|
|
public IEnumerator WaitForPending()
|
|
{
|
|
while (!_disposed)
|
|
{
|
|
bool pending = false;
|
|
foreach (var entry in _atlases.Values) pending |= entry.Loading;
|
|
if (!pending) yield break;
|
|
yield return null;
|
|
}
|
|
}
|
|
|
|
public IEnumerator LoadSpriteAsync(string relativeFolder, string spriteName, Action<Sprite> onLoaded)
|
|
{
|
|
if (string.IsNullOrEmpty(spriteName) || _disposed) { onLoaded?.Invoke(null); yield break; }
|
|
string key = UiAtlasPaths.GetAtlasPath(relativeFolder) + "|" + spriteName;
|
|
if (_sprites.TryGetValue(key, out var sprite) && sprite != null)
|
|
{
|
|
onLoaded?.Invoke(sprite);
|
|
yield break;
|
|
}
|
|
SpriteAtlas atlas = null;
|
|
yield return LoadAtlasAsync(relativeFolder, loaded => atlas = loaded);
|
|
if (_disposed) yield break;
|
|
if (atlas != null)
|
|
{
|
|
// GetSprite 创建的克隆需缓存并在销毁时释放。
|
|
if (!_sprites.TryGetValue(key, out sprite) || sprite == null)
|
|
{
|
|
sprite = atlas.GetSprite(spriteName);
|
|
if (sprite != null) _sprites[key] = sprite;
|
|
}
|
|
}
|
|
if (sprite == null) Debug.LogWarning($"[UIAtlas] Sprite unavailable: {relativeFolder}/{spriteName}");
|
|
onLoaded?.Invoke(sprite);
|
|
}
|
|
|
|
public IEnumerator SetSpriteAsync(Image image, string relativeFolder, string spriteName, Action<Sprite> onLoaded = null)
|
|
{
|
|
if (image == null || _disposed) yield break;
|
|
var request = new object();
|
|
_imageRequests[image] = request;
|
|
Sprite sprite = null;
|
|
yield return LoadSpriteAsync(relativeFolder, spriteName, loaded => sprite = loaded);
|
|
// 同一个 Image 后来的请求优先,旧请求不能覆盖新图片。
|
|
if (_disposed || !_imageRequests.TryGetValue(image, out var current) || current != request) yield break;
|
|
_imageRequests.Remove(image);
|
|
if (image == null) yield break;
|
|
image.sprite = sprite;
|
|
onLoaded?.Invoke(sprite);
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
_disposed = true;
|
|
SpriteAtlasManager.atlasRequested -= RequestAtlas;
|
|
StopAllCoroutines();
|
|
foreach (var sprite in _sprites.Values) if (sprite != null) Destroy(sprite);
|
|
_sprites.Clear();
|
|
foreach (var entry in _atlases.Values) entry.Handle?.Release();
|
|
_atlases.Clear();
|
|
_imageRequests.Clear();
|
|
}
|
|
}
|
|
}
|