Files
zeling_v2/Assets/_Game/Scripts/UI/Controls/UITabGroup.cs
2026-06-06 09:00:11 +08:00

158 lines
6.4 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 System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using BaseGames.Core.Events;
namespace BaseGames.UI
{
/// <summary>
/// 可复用标签组(从 <see cref="BaseGames.UI.Inventory.InventoryHubPanel"/> 的 Tab 逻辑提取)。
///
/// 职责:管理一组 Tab内容根 + 头部按钮 + 高亮),处理 Next/Prev/Select 切换、
/// 头部高亮、焦点委托(内容若实现 <see cref="IFocusable"/> 则接管,否则聚焦头部按钮),
/// 可选跨重建记忆当前 Tab并通过 C# 事件 + 可选 SO 频道广播索引。
///
/// 解耦:不引用任何具体 Tab 类型,只持有 Tab 根 <see cref="GameObject"/>。
/// 由宿主面板在 OnEnable 中调用 <see cref="Activate"/>,并把肩键频道接到 <see cref="Next"/>/<see cref="Prev"/>。
/// </summary>
public class UITabGroup : MonoBehaviour
{
[Serializable]
public struct Tab
{
[Tooltip("Tab 内容根节点(激活时 SetActive(true))。")]
public GameObject content;
[Tooltip("Tab 头部按钮(点击跳转;高亮显示当前选中)。可空。")]
public Button headerButton;
[Tooltip("Tab 头部高亮节点(选中时 SetActive(true))。可空。")]
public GameObject headerHighlight;
}
[Header("Tabs按显示顺序")]
[SerializeField] private Tab[] _tabs;
[Tooltip("默认打开的 Tab 索引。")]
[SerializeField] private int _defaultIndex = 0;
[Tooltip("是否记住上次停留的 Tab跨面板重开。")]
[SerializeField] private bool _rememberLast = true;
[Tooltip("跨重建记忆用的键;为空则只在本实例存活期间记忆。")]
[SerializeField] private string _persistKey = "";
[Header("Event Channels可选")]
[Tooltip("当前 Tab 变化时广播索引。可空。")]
[SerializeField] private IntEventChannelSO _onTabChanged;
/// <summary>当前 Tab 变化时触发(参数为新索引)。</summary>
public event Action<int> TabChanged;
public int CurrentIndex { get; private set; } = -1;
public int TabCount => _tabs?.Length ?? 0;
// 跨重建记忆(静态:面板/组件被销毁重建后仍保留)
private static readonly Dictionary<string, int> s_persist = new();
private bool _wired;
private void Awake() => EnsureWired();
private void EnsureWired()
{
if (_wired || _tabs == null) return;
for (int i = 0; i < _tabs.Length; i++)
{
int captured = i;
if (_tabs[i].headerButton != null)
{
_tabs[i].headerButton.onClick.RemoveAllListeners();
_tabs[i].headerButton.onClick.AddListener(() => Select(captured));
}
if (_tabs[i].content != null) _tabs[i].content.SetActive(false);
if (_tabs[i].headerHighlight != null) _tabs[i].headerHighlight.SetActive(false);
}
_wired = true;
}
/// <summary>激活标签组并打开起始 Tab默认或记忆。宿主面板在 OnEnable 中调用。</summary>
public void Activate()
{
EnsureWired();
if (TabCount == 0) return;
int start = _rememberLast && TryGetPersisted(out var p) ? p : _defaultIndex;
CurrentIndex = -1; // 强制 Select 执行切换
Select(Mathf.Clamp(start, 0, TabCount - 1));
}
public void Next() => Step(+1);
public void Prev() => Step(-1);
private void Step(int dir)
{
if (TabCount == 0) return;
int next = CurrentIndex;
for (int i = 0; i < _tabs.Length; i++) // 跳过未配置 content 的空槽
{
next = (next + dir + _tabs.Length) % _tabs.Length;
if (_tabs[next].content != null) break;
}
Select(next);
}
/// <summary>切换到指定 Tab无效或与当前相同则忽略。</summary>
public void Select(int index)
{
EnsureWired();
if (TabCount == 0) return;
index = Mathf.Clamp(index, 0, _tabs.Length - 1);
if (index == CurrentIndex) return;
// 关闭旧 Tab
if (CurrentIndex >= 0 && CurrentIndex < _tabs.Length)
{
if (_tabs[CurrentIndex].content != null) _tabs[CurrentIndex].content.SetActive(false);
if (_tabs[CurrentIndex].headerHighlight != null) _tabs[CurrentIndex].headerHighlight.SetActive(false);
}
CurrentIndex = index;
SetPersisted(index);
// 打开新 Tab
var tab = _tabs[index];
if (tab.content != null) tab.content.SetActive(true);
if (tab.headerHighlight != null) tab.headerHighlight.SetActive(true);
FocusCurrent();
_onTabChanged?.Raise(index);
TabChanged?.Invoke(index);
}
/// <summary>把焦点交给当前 Tab内容 IFocusable 优先,否则其头部按钮)。供宿主 OnFocusRestored 调用。</summary>
public void FocusCurrent()
{
if (CurrentIndex < 0 || CurrentIndex >= TabCount) return;
var tab = _tabs[CurrentIndex];
var focusable = tab.content != null ? tab.content.GetComponent<IFocusable>() : null;
if (focusable != null) focusable.OnFocusRestored();
else if (tab.headerButton != null && EventSystem.current != null)
EventSystem.current.SetSelectedGameObject(tab.headerButton.gameObject);
}
// ── 记忆 ──────────────────────────────────────────────────────────────
private bool TryGetPersisted(out int index)
{
if (!string.IsNullOrEmpty(_persistKey) && s_persist.TryGetValue(_persistKey, out index)) return true;
if (CurrentIndex >= 0) { index = CurrentIndex; return true; }
index = -1;
return false;
}
private void SetPersisted(int index)
{
if (!string.IsNullOrEmpty(_persistKey)) s_persist[_persistKey] = index;
}
}
}