Files
zeling_v2/Assets/_Game/Scripts/UI/Base/UIPanelBase.cs
2026-06-07 11:49:55 +08:00

130 lines
7.0 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using BaseGames.Core;
using BaseGames.Core.Events;
namespace BaseGames.UI
{
/// <summary>
/// UI 面板基类:统一面板生命周期、焦点恢复、进场淡入与事件订阅清理。
///
/// 消除各面板控制器PauseMenu / Settings / InventoryHub 等)重复的样板:
/// - OnEnable取服务 / 订阅 / 设默认选中 / 播淡入
/// - OnDisable清理订阅<see cref="_subs"/>/ 停动画
/// - <see cref="IFocusable.OnFocusRestored"/>:关闭子面板回到栈顶时聚焦首项
///
/// 与 <see cref="UIManager"/> 面板栈完全兼容UIManager 仍以 SetActive 驱动开关,
/// 本基类只在 OnEnable/OnDisable 加钩子,不改变栈语义。
///
/// 子类用法:重写 <see cref="OnPanelOpen"/>(订阅/刷新)与可选 <see cref="OnPanelClose"/>
/// 在 Inspector 指定 <see cref="_firstSelected"/>(手柄/键盘默认焦点);可选挂 CanvasGroup 做淡入。
/// </summary>
[DisallowMultipleComponent]
public abstract class UIPanelBase : MonoBehaviour, IFocusable
{
[Header("Panel Base")]
[Tooltip("打开 / 焦点恢复时默认选中的控件(手柄/键盘导航起点)。可空。")]
[SerializeField] protected Selectable _firstSelected;
[Tooltip("进场淡入用的 CanvasGroup可空为空则不淡入。")]
[SerializeField] protected CanvasGroup _canvasGroup;
[Tooltip("进场淡入时长。0 表示无淡入。")]
[SerializeField] protected float _fadeInDuration = 0.15f;
[Tooltip("打开时是否自动把焦点设到首项。")]
[SerializeField] protected bool _selectFirstOnEnable = true;
[Header("导航栈")]
[Tooltip("被压栈时正下方面板的处理方式Replace 停用下层整屏切换Modal 保留下层可见但屏蔽其交互(对话框)。")]
[SerializeField] protected PushMode _defaultMode = PushMode.Replace;
[Tooltip("是否允许 ESC / 手柄 B 取消本面板(栈顶时)。确认破坏性操作的根面板可关闭。")]
[SerializeField] protected bool _canCancel = true;
/// <summary>被压栈方式(<see cref="IUINavigator.Push"/> 未显式指定 mode 时采用)。</summary>
public PushMode DefaultMode => _defaultMode;
/// <summary>栈顶时 ESC / 手柄 B 是否可取消本面板。</summary>
public bool CanCancel => _canCancel;
/// <summary>键盘 / 手柄默认焦点项(<see cref="_firstSelected"/> 优先,否则 <see cref="ResolveFirstSelected"/>)。供导航器出/入栈聚焦。</summary>
public GameObject FirstSelectableGO
=> _firstSelected != null ? _firstSelected.gameObject : ResolveFirstSelected();
/// <summary>事件订阅容器OnDisable 自动清理。子类订阅用 <c>channel.Subscribe(..).AddTo(_subs)</c>。</summary>
protected readonly CompositeDisposable _subs = new();
private Coroutine _fadeRoutine;
// ── 生命周期 ──────────────────────────────────────────────────────────
protected virtual void OnEnable()
{
if (_canvasGroup == null) _canvasGroup = GetComponent<CanvasGroup>(); // 惰性解析(兼容运行时适配/未连线)
OnPanelOpen();
if (_canvasGroup != null && _fadeInDuration > 0f) PlayFadeIn();
if (_selectFirstOnEnable) FocusFirst();
}
protected virtual void OnDisable()
{
_subs.Clear();
if (_fadeRoutine != null) { StopCoroutine(_fadeRoutine); _fadeRoutine = null; }
OnPanelClose();
}
/// <summary>面板打开时调用OnEnable。子类在此取服务、订阅事件、刷新内容。</summary>
protected virtual void OnPanelOpen() { }
/// <summary>面板关闭时调用OnDisable_subs 已清理之后)。子类在此释放非 _subs 资源。</summary>
protected virtual void OnPanelClose() { }
// ── 导航栈交互屏蔽 ────────────────────────────────────────────────────
/// <summary>
/// 切换本面板"是否参与交互/导航"。导航器把模态对话框压在本面板之上时,
/// 以此屏蔽本面板(<see cref="CanvasGroup"/> interactable+blocksRaycasts=false——
/// 子控件 <c>Selectable.IsInteractable()</c> 随之为 false自动退出 Unity 导航图,
/// 杜绝方向键穿透到下层(无 CanvasGroup 时为空操作)。
/// </summary>
public void SetInteractableLayer(bool on)
{
if (_canvasGroup == null) return;
_canvasGroup.interactable = on;
_canvasGroup.blocksRaycasts = on;
}
// ── 焦点 ──────────────────────────────────────────────────────────────
/// <summary>本面板重新成为栈顶(上层出栈)时调用:默认聚焦首项。</summary>
public virtual void OnFocusGained() => FocusFirst();
/// <summary>本面板被上层压栈覆盖时调用(可选钩子,默认无操作)。</summary>
public virtual void OnFocusLost() { }
/// <summary>兼容旧 <see cref="IFocusable"/> 路径:等价于 <see cref="OnFocusGained"/>。</summary>
public virtual void OnFocusRestored() => OnFocusGained();
/// <summary>将 EventSystem 焦点设到首项(优先 <see cref="_firstSelected"/>,否则 <see cref="ResolveFirstSelected"/>)。</summary>
protected void FocusFirst()
{
var go = _firstSelected != null ? _firstSelected.gameObject : ResolveFirstSelected();
if (go != null && EventSystem.current != null)
EventSystem.current.SetSelectedGameObject(go);
}
/// <summary>子类可重写:动态决定首个选中项(如列表第一行),当 <see cref="_firstSelected"/> 未指定时使用。</summary>
protected virtual GameObject ResolveFirstSelected() => null;
// ── 工具 ──────────────────────────────────────────────────────────────
/// <summary>从 ServiceLocator 取服务(未注册返回 null。</summary>
protected static T GetService<T>() where T : class => ServiceLocator.GetOrDefault<T>();
private void PlayFadeIn()
{
if (_fadeRoutine != null) StopCoroutine(_fadeRoutine);
_canvasGroup.alpha = 0f;
_fadeRoutine = StartCoroutine(UITween.FadeCanvasGroup(_canvasGroup, 1f, _fadeInDuration));
}
}
}