新增 FlowScope Git 包雏形
This commit is contained in:
266
Packages/com.flowscope.gamecore/Runtime/UI/UIManager.cs
Normal file
266
Packages/com.flowscope.gamecore/Runtime/UI/UIManager.cs
Normal file
@@ -0,0 +1,266 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FlowScope.Resources;
|
||||
using UnityEngine;
|
||||
|
||||
namespace FlowScope.UI
|
||||
{
|
||||
public sealed class UIManager
|
||||
{
|
||||
private readonly IResourceService _resources;
|
||||
private readonly IUIPreloadService _preloadService;
|
||||
private readonly Dictionary<string, UILayer> _layers = new();
|
||||
|
||||
public UIManager(IResourceService resources, IUIPreloadService preloadService = null)
|
||||
{
|
||||
_resources = resources ?? throw new ArgumentNullException(nameof(resources));
|
||||
_preloadService = preloadService;
|
||||
}
|
||||
|
||||
public void RegisterLayer(
|
||||
string name,
|
||||
Canvas canvas,
|
||||
int sortOrder,
|
||||
PanelStrategy strategy = PanelStrategy.Destroy)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentException("Layer name is required.", nameof(name));
|
||||
}
|
||||
|
||||
if (canvas == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(canvas));
|
||||
}
|
||||
|
||||
_layers[name] = new UILayer(name, canvas, sortOrder, strategy);
|
||||
}
|
||||
|
||||
public async Task<TPanel> OpenAsync<TPanel, TViewModel>(
|
||||
TViewModel viewModel,
|
||||
CancellationToken cancellationToken)
|
||||
where TPanel : UIPanelBase<TViewModel>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var attribute = GetPanelAttribute(typeof(TPanel));
|
||||
if (!_layers.TryGetValue(attribute.Layer, out var layer))
|
||||
{
|
||||
throw new InvalidOperationException($"UI layer is not registered: {attribute.Layer}");
|
||||
}
|
||||
|
||||
var strategy = attribute.OverrideStrategy ?? layer.Strategy;
|
||||
if (strategy == PanelStrategy.Cache &&
|
||||
layer.TryGetCached(typeof(TPanel), out var cached) &&
|
||||
!cached.IsOpen)
|
||||
{
|
||||
return ReopenCached<TPanel, TViewModel>(cached, layer, viewModel);
|
||||
}
|
||||
|
||||
var path = ResolvePath(typeof(TPanel), attribute);
|
||||
var handle = await LoadPanelHandleAsync<TPanel>(path, cancellationToken);
|
||||
if (handle == null || handle.Asset == null)
|
||||
{
|
||||
handle?.Dispose();
|
||||
throw new InvalidOperationException($"UI panel resource is missing: {path}");
|
||||
}
|
||||
|
||||
GameObject instance = null;
|
||||
var ownsHandle = !IsPreloadedHandle<TPanel>(handle);
|
||||
try
|
||||
{
|
||||
instance = UnityEngine.Object.Instantiate(handle.Asset, layer.Canvas.transform, false);
|
||||
var panel = instance.GetComponent<TPanel>();
|
||||
if (panel == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"UI panel prefab '{path}' does not contain component {typeof(TPanel).Name}.");
|
||||
}
|
||||
|
||||
panel.Bind(viewModel);
|
||||
instance.SetActive(true);
|
||||
|
||||
var recordHandle = ownsHandle ? (IDisposable)handle : RetainedPanelHandle.Instance;
|
||||
var record = new UIPanelRecord(typeof(TPanel), layer.Name, panel, recordHandle, strategy);
|
||||
if (strategy == PanelStrategy.Cache)
|
||||
{
|
||||
layer.Cache(record);
|
||||
}
|
||||
|
||||
layer.Push(record);
|
||||
return panel;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (instance != null)
|
||||
{
|
||||
UnityEngine.Object.Destroy(instance);
|
||||
}
|
||||
|
||||
if (ownsHandle)
|
||||
{
|
||||
handle.Dispose();
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void Close<TPanel>()
|
||||
{
|
||||
var panelType = typeof(TPanel);
|
||||
foreach (var layer in _layers.Values)
|
||||
{
|
||||
if (!layer.ContainsOpen(panelType, out var record))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ReferenceEquals(layer.Peek(), record))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot close non-top UI panel: {panelType.Name}");
|
||||
}
|
||||
|
||||
CloseTop(layer);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void CloseLayer(string layerName)
|
||||
{
|
||||
if (!_layers.TryGetValue(layerName, out var layer))
|
||||
{
|
||||
throw new InvalidOperationException($"UI layer is not registered: {layerName}");
|
||||
}
|
||||
|
||||
while (layer.Count > 0)
|
||||
{
|
||||
CloseTop(layer);
|
||||
}
|
||||
}
|
||||
|
||||
public void CloseAll()
|
||||
{
|
||||
foreach (var layer in _layers.Values)
|
||||
{
|
||||
while (layer.Count > 0)
|
||||
{
|
||||
CloseTop(layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsOpen<TPanel>()
|
||||
{
|
||||
var panelType = typeof(TPanel);
|
||||
foreach (var layer in _layers.Values)
|
||||
{
|
||||
if (layer.ContainsOpen(panelType, out _))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static TPanel ReopenCached<TPanel, TViewModel>(
|
||||
UIPanelRecord record,
|
||||
UILayer layer,
|
||||
TViewModel viewModel)
|
||||
where TPanel : UIPanelBase<TViewModel>
|
||||
{
|
||||
var panel = (TPanel)record.Panel;
|
||||
panel.Bind(viewModel);
|
||||
panel.gameObject.SetActive(true);
|
||||
layer.Push(record);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private static void CloseTop(UILayer layer)
|
||||
{
|
||||
var record = layer.Pop();
|
||||
record.Panel.Unbind();
|
||||
|
||||
if (record.Strategy == PanelStrategy.Cache)
|
||||
{
|
||||
record.Panel.gameObject.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
layer.RemoveCached(record.PanelType);
|
||||
UnityEngine.Object.Destroy(record.Panel.gameObject);
|
||||
record.Handle.Dispose();
|
||||
}
|
||||
|
||||
private async Task<IResourceHandle<GameObject>> LoadPanelHandleAsync<TPanel>(
|
||||
string path,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (_preloadService != null &&
|
||||
_preloadService.TryGetPreloadedHandle<TPanel>(out var preloadedHandle))
|
||||
{
|
||||
return preloadedHandle;
|
||||
}
|
||||
|
||||
return await _resources.LoadAsync<GameObject>(path, cancellationToken);
|
||||
}
|
||||
|
||||
private bool IsPreloadedHandle<TPanel>(IResourceHandle<GameObject> handle)
|
||||
{
|
||||
return _preloadService != null &&
|
||||
_preloadService.TryGetPreloadedHandle<TPanel>(out var preloadedHandle) &&
|
||||
ReferenceEquals(preloadedHandle, handle);
|
||||
}
|
||||
|
||||
internal static UIPanelAttribute GetPanelAttribute(Type panelType)
|
||||
{
|
||||
var attribute = (UIPanelAttribute)Attribute.GetCustomAttribute(
|
||||
panelType,
|
||||
typeof(UIPanelAttribute));
|
||||
|
||||
if (attribute == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"UI panel must declare UIPanelAttribute: {panelType.Name}");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(attribute.Layer))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"UI panel layer is required: {panelType.Name}");
|
||||
}
|
||||
|
||||
return attribute;
|
||||
}
|
||||
|
||||
internal static string ResolvePath(Type panelType, UIPanelAttribute attribute)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(attribute.Path))
|
||||
{
|
||||
return attribute.Path;
|
||||
}
|
||||
|
||||
var name = panelType.Name;
|
||||
const string suffix = "Panel";
|
||||
if (name.EndsWith(suffix, StringComparison.Ordinal))
|
||||
{
|
||||
name = name.Substring(0, name.Length - suffix.Length);
|
||||
}
|
||||
|
||||
return $"UI/{name}/Prefab";
|
||||
}
|
||||
|
||||
private sealed class RetainedPanelHandle : IDisposable
|
||||
{
|
||||
public static readonly RetainedPanelHandle Instance = new();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user