748 lines
30 KiB
C#
748 lines
30 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using Logic.Multilingual;
|
||
using TH1_Core.Events;
|
||
using TH1_Logic.MatchConfig;
|
||
using TH1_UI.View.Outside.WikiSub;
|
||
using TMPro;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
|
||
namespace TH1_UI.View.Outside
|
||
{
|
||
// 一级 Big 分类 + 该分类下的二级 Small 分类
|
||
[Serializable]
|
||
public class BigSelectGroup
|
||
{
|
||
public WikiType BigType;
|
||
public List<WikiType> SmallTypes = new List<WikiType>();
|
||
}
|
||
|
||
public class UIOutsideWikiView : Base.View
|
||
{
|
||
[Header("Close")]
|
||
public Button CloseButton;
|
||
public Button BlockerButton;
|
||
|
||
[Header("BigSelect")]
|
||
// 二级嵌套:每个 BigSelectGroup 包含一个 Big 类型和它对应的 Small 类型列表
|
||
public List<BigSelectGroup> SelectGroups = new List<BigSelectGroup>();
|
||
public BigSelectButton BigSelectButtonPrefab;
|
||
public Transform BigSelectArea;
|
||
|
||
[Header("SmallSelect")]
|
||
public SmallSelectButton SmallSelectButtonPrefab;
|
||
public Transform SmallSelectArea;
|
||
|
||
[Header("WikiItem List")]
|
||
public WikiListItem WikiListItemPrefab;
|
||
public Transform WikiListContent;
|
||
|
||
[Header("WikiContentArea")]
|
||
public TextMeshProUGUI TitleText;
|
||
|
||
// 按 WikiType 分组的 Icon 容器:父对象用于激活/隐藏,Image 用于替换 sprite。
|
||
// 资源大小由外部预先调好,这里只做激活和 sprite 替换,不调用 IconSizingUtility。
|
||
[Header("Typed Icon Groups")]
|
||
public GameObject UnitIconGroup;
|
||
public Image UnitIconImage;
|
||
public GameObject SkillIconGroup;
|
||
public Image SkillIconImage;
|
||
// Skill 分组的底色:根据 SkillViewType 走 SkillDataAssets.GetBGColor 着色
|
||
public Image SkillIconBg;
|
||
public GameObject ActionIconGroup;
|
||
public Image ActionIconImage;
|
||
public GameObject ResourceIconGroup;
|
||
public Image ResourceIconImage;
|
||
// Force / Hero 共用:Force 使用 PlayerInfo.LeaderAvatar,Hero 使用 HeroDataAssets.HeroAvatar
|
||
public GameObject PlayerIconContainer;
|
||
public Image PlayerIconImage;
|
||
|
||
// Tag 文本:把 wikiItem.Types 转成多语言名后用 "," 拼接
|
||
[Header("Tag")]
|
||
public TextMeshProUGUI TagText;
|
||
|
||
public WikiDescGroup WikiDescGroupPrefab;
|
||
public Transform DescArea;
|
||
|
||
public ViDelegateAssisstant.Dele<WikiType> OnBigSelectClick;
|
||
public ViDelegateAssisstant.Dele<WikiType> OnSmallSelectClick;
|
||
public ViDelegateAssisstant.Dele<WikiItem> OnWikiListItemClick;
|
||
public ViDelegateAssisstant.Dele OnBtnCloseClick;
|
||
|
||
private readonly List<BigSelectButton> _bigSelectButtons = new List<BigSelectButton>();
|
||
private readonly List<SmallSelectButton> _smallSelectButtons = new List<SmallSelectButton>();
|
||
private readonly List<WikiListItem> _wikiListItems = new List<WikiListItem>();
|
||
private readonly List<WikiDescGroup> _wikiDescGroups = new List<WikiDescGroup>();
|
||
|
||
protected override void OnInit()
|
||
{
|
||
base.OnInit();
|
||
InitBigSelectButtons();
|
||
ClearSmallSelectButtons(); // Small 区域初始为空,等 Big 被点击后再填充
|
||
ClearWikiList();
|
||
ClearTitle();
|
||
InitCloseButtons();
|
||
WikiSub.WikiDescGroup.OnRequestWikiJump += HandleWikiJumpRequest;
|
||
}
|
||
|
||
private void InitCloseButtons()
|
||
{
|
||
if (CloseButton != null)
|
||
{
|
||
CloseButton.onClick.RemoveAllListeners();
|
||
CloseButton.onClick.AddListener(OnClose);
|
||
}
|
||
if (BlockerButton != null)
|
||
{
|
||
BlockerButton.onClick.RemoveAllListeners();
|
||
BlockerButton.onClick.AddListener(OnClose);
|
||
}
|
||
}
|
||
|
||
private void OnClose()
|
||
{
|
||
ViDelegateAssisstant.Invoke(OnBtnCloseClick);
|
||
}
|
||
|
||
public void SetContent(ShowUIOutsideWiki evt)
|
||
{
|
||
ClearTitle();
|
||
ClearWikiList();
|
||
}
|
||
|
||
private void InitBigSelectButtons()
|
||
{
|
||
ClearBigSelectButtons();
|
||
if (BigSelectButtonPrefab == null || BigSelectArea == null || SelectGroups == null) return;
|
||
|
||
for (int i = 0; i < SelectGroups.Count; i++)
|
||
{
|
||
var group = SelectGroups[i];
|
||
if (group == null) continue;
|
||
var button = Instantiate(BigSelectButtonPrefab, BigSelectArea);
|
||
button.SetContent(group.BigType);
|
||
button.OnClick += HandleBigSelectButtonClick;
|
||
_bigSelectButtons.Add(button);
|
||
}
|
||
// BigSelectArea 是 LayoutGroup,动态增删后需要强制重建 Layout
|
||
RebuildLayout(BigSelectArea as RectTransform);
|
||
}
|
||
|
||
// 由 Controller 在 Big 被点击后调用:根据当前 Big 重新生成对应的 Small 按钮。
|
||
// 返回值表示该 Big 下是否存在 Small;不存在时 SmallSelectArea 会被隐藏。
|
||
public bool ShowSmallSelectButtonsForBig(WikiType bigType)
|
||
{
|
||
ClearSmallSelectButtons();
|
||
if (SelectGroups == null) { SetSmallAreaActive(false); return false; }
|
||
|
||
for (int i = 0; i < SelectGroups.Count; i++)
|
||
{
|
||
var group = SelectGroups[i];
|
||
if (group == null) continue;
|
||
if (!group.BigType.Equals(bigType)) continue;
|
||
|
||
bool hasSmalls = group.SmallTypes != null && group.SmallTypes.Count > 0;
|
||
SetSmallAreaActive(hasSmalls);
|
||
if (!hasSmalls) return false;
|
||
if (SmallSelectButtonPrefab == null || SmallSelectArea == null) return false;
|
||
|
||
for (int j = 0; j < group.SmallTypes.Count; j++)
|
||
{
|
||
var button = Instantiate(SmallSelectButtonPrefab, SmallSelectArea);
|
||
button.SetContent(group.SmallTypes[j]);
|
||
button.OnClick += HandleSmallSelectButtonClick;
|
||
_smallSelectButtons.Add(button);
|
||
}
|
||
// SmallSelectArea 是 LayoutGroup,动态增删后需要强制重建 Layout
|
||
RebuildLayout(SmallSelectArea as RectTransform);
|
||
return true;
|
||
}
|
||
// 没有匹配到 group
|
||
SetSmallAreaActive(false);
|
||
return false;
|
||
}
|
||
|
||
private void SetSmallAreaActive(bool active)
|
||
{
|
||
if (SmallSelectArea == null) return;
|
||
if (SmallSelectArea.gameObject.activeSelf != active)
|
||
SmallSelectArea.gameObject.SetActive(active);
|
||
}
|
||
|
||
// 强制立即重建 LayoutGroup 的布局,避免动态增删子对象后一帧残留的尺寸/位置(spacing)问题。
|
||
// 仅 ForceRebuildLayoutImmediate 在 LayoutGroup 刚 SetActive 或子节点刚 Instantiate 时
|
||
// 经常算错 spacing,需要 toggle LayoutGroup 把内部脏状态彻底重置。
|
||
// 多层嵌套的 LayoutGroup + ContentSizeFitter 必须自底向上 Rebuild,
|
||
// 否则父 Fitter 在子还没算完尺寸时就先收尾,首帧布局会错位。
|
||
private static void RebuildLayout(RectTransform rt)
|
||
{
|
||
if (rt == null) return;
|
||
|
||
// 先 toggle 所有相关 LayoutGroup(含自身和子节点),强制其重建内部缓存
|
||
var groups = rt.GetComponentsInChildren<LayoutGroup>(true);
|
||
for (int i = 0; i < groups.Length; i++)
|
||
{
|
||
var g = groups[i];
|
||
if (g == null) continue;
|
||
g.enabled = false;
|
||
g.enabled = true;
|
||
}
|
||
|
||
// 收集所有需要重建的 RectTransform:自身 + 任何含 LayoutGroup 或 ContentSizeFitter 的子节点
|
||
var rebuildTargets = new List<RectTransform> { rt };
|
||
var fitters = rt.GetComponentsInChildren<ContentSizeFitter>(true);
|
||
for (int i = 0; i < fitters.Length; i++)
|
||
{
|
||
var t = fitters[i] != null ? fitters[i].transform as RectTransform : null;
|
||
if (t != null && !rebuildTargets.Contains(t)) rebuildTargets.Add(t);
|
||
}
|
||
for (int i = 0; i < groups.Length; i++)
|
||
{
|
||
var t = groups[i] != null ? groups[i].transform as RectTransform : null;
|
||
if (t != null && !rebuildTargets.Contains(t)) rebuildTargets.Add(t);
|
||
}
|
||
|
||
// 自底向上:按层级深度降序排序,先 rebuild 叶子,再 rebuild 父
|
||
rebuildTargets.Sort((a, b) => GetDepth(b).CompareTo(GetDepth(a)));
|
||
|
||
Canvas.ForceUpdateCanvases();
|
||
for (int i = 0; i < rebuildTargets.Count; i++)
|
||
{
|
||
if (rebuildTargets[i] == null) continue;
|
||
LayoutRebuilder.ForceRebuildLayoutImmediate(rebuildTargets[i]);
|
||
}
|
||
}
|
||
|
||
private static int GetDepth(Transform t)
|
||
{
|
||
int d = 0;
|
||
while (t != null && t.parent != null) { t = t.parent; d++; }
|
||
return d;
|
||
}
|
||
|
||
public void RefreshWikiList(List<WikiItem> wikiItems)
|
||
{
|
||
ClearWikiList();
|
||
if (WikiListItemPrefab == null || WikiListContent == null || wikiItems == null) return;
|
||
|
||
for (int i = 0; i < wikiItems.Count; i++)
|
||
{
|
||
var item = Instantiate(WikiListItemPrefab, WikiListContent);
|
||
item.SetContent(wikiItems[i]);
|
||
item.OnClick += HandleWikiListItemClick;
|
||
_wikiListItems.Add(item);
|
||
}
|
||
// WikiListContent 是 LayoutGroup,动态增删后需要强制重建 Layout
|
||
RebuildLayout(WikiListContent as RectTransform);
|
||
}
|
||
|
||
// Stage 1 行为:没有同时选中 Big 和 Small 时,由 Controller 调用此方法清空列表。
|
||
public void ClearWikiList()
|
||
{
|
||
for (int i = 0; i < _wikiListItems.Count; i++)
|
||
{
|
||
if (_wikiListItems[i] == null) continue;
|
||
_wikiListItems[i].ClearCallback();
|
||
DetachAndDestroy(_wikiListItems[i].gameObject);
|
||
}
|
||
_wikiListItems.Clear();
|
||
}
|
||
|
||
public void SetTitle(WikiItem wikiItem)
|
||
{
|
||
if (wikiItem == null)
|
||
{
|
||
ClearTitle();
|
||
return;
|
||
}
|
||
|
||
if (TitleText != null)
|
||
{
|
||
var ml = TitleText.GetComponent<MultilingualTextMono>();
|
||
if (ml != null) ml.ID = 0;
|
||
if (!string.IsNullOrEmpty(wikiItem.Name) && uint.TryParse(wikiItem.Name, out _))
|
||
MultilingualManager.Instance.SetUIText(TitleText, wikiItem.Name);
|
||
else
|
||
TitleText.text = wikiItem.Name ?? string.Empty;
|
||
}
|
||
|
||
ApplyContentIcon(wikiItem);
|
||
ApplyTagText(wikiItem);
|
||
RefreshDescArea(wikiItem);
|
||
}
|
||
|
||
// 把 wikiItem.Types 里每一个枚举转成多语言名(通过 WikiData.GetTypeName 拿 key,
|
||
// 再走 MultilingualManager 转成当前语种),用 ", " 拼成一长串塞进 TagText。
|
||
// 没有任何 tag 时清空文本。
|
||
private void ApplyTagText(WikiItem wikiItem)
|
||
{
|
||
if (TagText == null) return;
|
||
|
||
// 与 TitleText 同样的处理:清掉可能存在的 MultilingualTextMono 绑定,
|
||
// 否则下一帧会被它覆盖回去
|
||
var ml = TagText.GetComponent<MultilingualTextMono>();
|
||
if (ml != null) ml.ID = 0;
|
||
|
||
if (wikiItem == null || wikiItem.Types == null || wikiItem.Types.Count == 0)
|
||
{
|
||
TagText.text = string.Empty;
|
||
return;
|
||
}
|
||
|
||
var wikiData = Table.Instance != null ? Table.Instance.WikiData : null;
|
||
var sb = new System.Text.StringBuilder();
|
||
for (int i = 0; i < wikiItem.Types.Count; i++)
|
||
{
|
||
string key = wikiData != null ? wikiData.GetTypeName(wikiItem.Types[i]) : null;
|
||
if (string.IsNullOrEmpty(key)) continue;
|
||
|
||
string text = key;
|
||
if (uint.TryParse(key, out _))
|
||
text = MultilingualManager.Instance.GetMultilingualText(key);
|
||
|
||
if (string.IsNullOrEmpty(text)) continue;
|
||
if (sb.Length > 0) sb.Append(", ");
|
||
sb.Append(text);
|
||
}
|
||
TagText.text = sb.ToString();
|
||
}
|
||
|
||
private void ApplyContentIcon(WikiItem wikiItem)
|
||
{
|
||
// 按类型挑出目标分组:5 个分组(Unit/Skill/Action/Resource/Player)择一激活
|
||
// wikiItem 无效、Icon 为 null 或都不命中类型时全部隐藏
|
||
ResolveTypedIconGroup(wikiItem, out var targetGroup, out var targetImage);
|
||
|
||
// 保护:sprite 为 null 时(任何分组都一样),不显示该分组
|
||
bool hasSprite = wikiItem != null && wikiItem.Icon != null;
|
||
if (!hasSprite)
|
||
{
|
||
targetGroup = null;
|
||
targetImage = null;
|
||
}
|
||
|
||
SetGroupActive(UnitIconGroup, targetGroup);
|
||
SetGroupActive(SkillIconGroup, targetGroup);
|
||
SetGroupActive(ActionIconGroup, targetGroup);
|
||
SetGroupActive(ResourceIconGroup, targetGroup);
|
||
SetGroupActive(PlayerIconContainer, targetGroup);
|
||
|
||
if (targetGroup != null && targetImage != null && wikiItem != null)
|
||
{
|
||
targetImage.sprite = wikiItem.Icon;
|
||
if (!targetImage.gameObject.activeSelf) targetImage.gameObject.SetActive(true);
|
||
}
|
||
|
||
// Skill 分组命中时根据 SkillViewType 给底色上色(其它分组不处理)
|
||
if (targetGroup == SkillIconGroup)
|
||
ApplySkillBgColor(wikiItem);
|
||
}
|
||
|
||
// 从 wikiItem.Types 里反推 SkillViewType(与 WikiTagRules.ForSkill 的映射相反),
|
||
// 然后查 SkillDataAssets.GetBGColor 设置到 SkillIconBg。
|
||
// 没有 SmallTypeSkill* 时退回 SkillViewType.Normal。
|
||
private void ApplySkillBgColor(WikiItem wikiItem)
|
||
{
|
||
if (SkillIconBg == null) return;
|
||
|
||
var viewType = SkillViewType.Normal;
|
||
if (wikiItem != null && wikiItem.Types != null)
|
||
{
|
||
for (int i = 0; i < wikiItem.Types.Count; i++)
|
||
{
|
||
switch (wikiItem.Types[i])
|
||
{
|
||
case WikiType.SmallTypeSkillNormal: viewType = SkillViewType.Normal; break;
|
||
case WikiType.SmallTypeSkillSpecial: viewType = SkillViewType.Special; break;
|
||
case WikiType.SmallTypeSkillUnique: viewType = SkillViewType.Unique; break;
|
||
case WikiType.SmallTypeSkillPositive: viewType = SkillViewType.Positive; break;
|
||
case WikiType.SmallTypeSkillNegative: viewType = SkillViewType.Negative; break;
|
||
}
|
||
}
|
||
}
|
||
|
||
var skillAssets = Table.Instance != null ? Table.Instance.SkillDataAssets : null;
|
||
if (skillAssets == null) return;
|
||
// 图鉴里没有 HasTimeLimit 概念,传 false
|
||
SkillIconBg.color = skillAssets.GetBGColor(viewType, false);
|
||
}
|
||
|
||
// 当前分组只激活与 target 同一引用的那个,其它统一关闭
|
||
private static void SetGroupActive(GameObject group, GameObject target)
|
||
{
|
||
if (group == null) return;
|
||
bool active = group != null && group == target;
|
||
if (group.activeSelf != active) group.SetActive(active);
|
||
}
|
||
|
||
// 按 WikiItem.Types 决定使用哪个分组:
|
||
// BigTypeUnit → UnitIconGroup
|
||
// BigTypeHero / BigTypeForce → PlayerIconContainer(Hero=HeroAvatar, Force=LeaderAvatar)
|
||
// BigTypeSkill → SkillIconGroup
|
||
// BigTypeAction → ActionIconGroup
|
||
// BigTypeResource / BigTypeBuilding → ResourceIconGroup
|
||
// 都不命中时 outGroup/outImage 为 null,所有分组全部隐藏
|
||
private void ResolveTypedIconGroup(WikiItem wikiItem, out GameObject outGroup, out Image outImage)
|
||
{
|
||
outGroup = null;
|
||
outImage = null;
|
||
if (wikiItem == null || wikiItem.Types == null) return;
|
||
|
||
for (int i = 0; i < wikiItem.Types.Count; i++)
|
||
{
|
||
switch (wikiItem.Types[i])
|
||
{
|
||
case WikiType.BigTypeUnit:
|
||
outGroup = UnitIconGroup; outImage = UnitIconImage; return;
|
||
case WikiType.BigTypeHero:
|
||
case WikiType.BigTypeForce:
|
||
outGroup = PlayerIconContainer; outImage = PlayerIconImage; return;
|
||
case WikiType.BigTypeSkill:
|
||
outGroup = SkillIconGroup; outImage = SkillIconImage; return;
|
||
case WikiType.BigTypeAction:
|
||
ResolveActionIconGroup(wikiItem, out outGroup, out outImage);
|
||
return;
|
||
case WikiType.BigTypeResource:
|
||
case WikiType.BigTypeBuilding:
|
||
outGroup = ResourceIconGroup; outImage = ResourceIconImage; return;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Action 类下,根据 wikiItem.IconSizeType 决定具体使用哪个 typed group:
|
||
// Building / Ground / MountainBuilding / Resource / Defense → ResourceIconGroup
|
||
// _256x256 → SkillIconGroup
|
||
// Unit → UnitIconGroup
|
||
private void ResolveActionIconGroup(WikiItem wikiItem, out GameObject outGroup, out Image outImage)
|
||
{
|
||
outGroup = null;
|
||
outImage = null;
|
||
if (wikiItem == null) return;
|
||
|
||
switch (wikiItem.IconSizeType)
|
||
{
|
||
case IconViewSizeType.Building:
|
||
case IconViewSizeType.Ground:
|
||
case IconViewSizeType.MountainBuilding:
|
||
case IconViewSizeType.Resource:
|
||
case IconViewSizeType.Defense:
|
||
outGroup = ResourceIconGroup; outImage = ResourceIconImage; return;
|
||
case IconViewSizeType._256x256:
|
||
outGroup = SkillIconGroup; outImage = SkillIconImage; return;
|
||
case IconViewSizeType.Unit:
|
||
outGroup = UnitIconGroup; outImage = UnitIconImage; return;
|
||
}
|
||
}
|
||
|
||
public void ClearTitle()
|
||
{
|
||
if (TitleText != null)
|
||
{
|
||
var ml = TitleText.GetComponent<MultilingualTextMono>();
|
||
if (ml != null) ml.ID = 0;
|
||
TitleText.text = string.Empty;
|
||
}
|
||
ApplyContentIcon(null);
|
||
ApplyTagText(null);
|
||
ClearDescArea();
|
||
}
|
||
|
||
private void RefreshDescArea(WikiItem wikiItem)
|
||
{
|
||
ClearDescArea();
|
||
if (wikiItem == null) return;
|
||
if (WikiDescGroupPrefab == null || DescArea == null) return;
|
||
var descItems = wikiItem.DescItems;
|
||
if (descItems == null) return;
|
||
|
||
for (int i = 0; i < descItems.Count; i++)
|
||
{
|
||
var descItem = descItems[i];
|
||
if (descItem == null) continue;
|
||
var group = Instantiate(WikiDescGroupPrefab, DescArea);
|
||
// SetContent 返回 false 表示该 desc 无可显示内容(如 HeroUpgrade 当级无任务),
|
||
// 直接销毁本 group,避免 LayoutGroup 中残留空段
|
||
if (!group.SetContent(wikiItem, descItem))
|
||
{
|
||
DetachAndDestroy(group.gameObject);
|
||
continue;
|
||
}
|
||
_wikiDescGroups.Add(group);
|
||
}
|
||
|
||
// DescArea 通常也是 LayoutGroup,动态增删后强制重建一次
|
||
RebuildLayout(DescArea as RectTransform);
|
||
}
|
||
|
||
private void ClearDescArea()
|
||
{
|
||
for (int i = 0; i < _wikiDescGroups.Count; i++)
|
||
{
|
||
if (_wikiDescGroups[i] == null) continue;
|
||
DetachAndDestroy(_wikiDescGroups[i].gameObject);
|
||
}
|
||
_wikiDescGroups.Clear();
|
||
}
|
||
|
||
public void OnCloseView()
|
||
{
|
||
ClearWikiList();
|
||
ClearTitle();
|
||
}
|
||
|
||
// 触发第一个 BigSelectButton 的点击;用于 OnOpen 时设置默认选中
|
||
public bool SelectFirstBig()
|
||
{
|
||
for (int i = 0; i < _bigSelectButtons.Count; i++)
|
||
{
|
||
var btn = _bigSelectButtons[i];
|
||
if (btn == null || btn.Button == null) continue;
|
||
btn.Button.onClick.Invoke();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 触发第一个 SmallSelectButton 的点击(若当前 Big 下有 Small 则点);用于 Big 选中后默认选 Small
|
||
public bool SelectFirstSmallIfAny()
|
||
{
|
||
for (int i = 0; i < _smallSelectButtons.Count; i++)
|
||
{
|
||
var btn = _smallSelectButtons[i];
|
||
if (btn == null || btn.Button == null) continue;
|
||
btn.Button.onClick.Invoke();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 触发第一个 WikiListItem 的点击;用于刷新列表后默认展示首项 Content
|
||
public bool SelectFirstWikiListItemIfAny()
|
||
{
|
||
for (int i = 0; i < _wikiListItems.Count; i++)
|
||
{
|
||
var item = _wikiListItems[i];
|
||
if (item == null || item.Button == null) continue;
|
||
item.Button.onClick.Invoke();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private void HandleBigSelectButtonClick(BigSelectButton button)
|
||
{
|
||
for (int i = 0; i < _bigSelectButtons.Count; i++)
|
||
{
|
||
if (_bigSelectButtons[i] == null) continue;
|
||
_bigSelectButtons[i].SetSelected(_bigSelectButtons[i] == button);
|
||
}
|
||
ViDelegateAssisstant.Invoke(OnBigSelectClick, button.WikiType);
|
||
}
|
||
|
||
private void HandleSmallSelectButtonClick(SmallSelectButton button)
|
||
{
|
||
for (int i = 0; i < _smallSelectButtons.Count; i++)
|
||
{
|
||
if (_smallSelectButtons[i] == null) continue;
|
||
_smallSelectButtons[i].SetSelected(_smallSelectButtons[i] == button);
|
||
}
|
||
ViDelegateAssisstant.Invoke(OnSmallSelectClick, button.WikiType);
|
||
}
|
||
|
||
private void HandleWikiListItemClick(WikiListItem item)
|
||
{
|
||
// 互斥切换选中态:被点中的高亮,其余取消
|
||
for (int i = 0; i < _wikiListItems.Count; i++)
|
||
{
|
||
if (_wikiListItems[i] == null) continue;
|
||
_wikiListItems[i].SetSelected(_wikiListItems[i] == item);
|
||
}
|
||
ViDelegateAssisstant.Invoke(OnWikiListItemClick, item.WikiItem);
|
||
}
|
||
|
||
private void ClearBigSelectButtons()
|
||
{
|
||
for (int i = 0; i < _bigSelectButtons.Count; i++)
|
||
{
|
||
if (_bigSelectButtons[i] == null) continue;
|
||
_bigSelectButtons[i].ClearCallback();
|
||
DetachAndDestroy(_bigSelectButtons[i].gameObject);
|
||
}
|
||
_bigSelectButtons.Clear();
|
||
}
|
||
|
||
private void ClearSmallSelectButtons()
|
||
{
|
||
for (int i = 0; i < _smallSelectButtons.Count; i++)
|
||
{
|
||
if (_smallSelectButtons[i] == null) continue;
|
||
_smallSelectButtons[i].ClearCallback();
|
||
DetachAndDestroy(_smallSelectButtons[i].gameObject);
|
||
}
|
||
_smallSelectButtons.Clear();
|
||
}
|
||
|
||
// Destroy 是异步的,子物体会留在父节点里直到帧末才被回收。
|
||
// 紧接着重建 LayoutGroup 会同时看到旧(待销毁)和新子物体,导致布局错乱。
|
||
// 销毁前先把子物体从父节点摘出来,让 LayoutGroup 立刻看不到它。
|
||
private static void DetachAndDestroy(GameObject go)
|
||
{
|
||
if (go == null) return;
|
||
if (go.transform != null) go.transform.SetParent(null, false);
|
||
Destroy(go);
|
||
}
|
||
|
||
protected override void OnDispose()
|
||
{
|
||
WikiSub.WikiDescGroup.OnRequestWikiJump -= HandleWikiJumpRequest;
|
||
ClearBigSelectButtons();
|
||
ClearSmallSelectButtons();
|
||
ClearWikiList();
|
||
ClearDescArea();
|
||
base.OnDispose();
|
||
}
|
||
|
||
// GridItem 点击跳转:在已有的 BigSelectGroups 内找一个能匹配 wikiId 对应 WikiItem.Types 的
|
||
// (Big, Small?) 组合,依次触发 BigSelectButton.click → SmallSelectButton.click → WikiList 选中。
|
||
// 找不到任何能容纳此 wikiId 的 group 就不切(按需求)。
|
||
private void HandleWikiJumpRequest(uint wikiId)
|
||
{
|
||
var wikiData = Table.Instance != null ? Table.Instance.WikiData : null;
|
||
if (wikiData == null) return;
|
||
var target = wikiData.GetById(wikiId);
|
||
if (target == null) return;
|
||
if (target.Types == null || target.Types.Count == 0) return;
|
||
|
||
if (!TryResolveJumpRoute(target, out var bigType, out var smallType, out var hasSmall)) return;
|
||
|
||
// 1) 触发 Big 按钮(SetSelected 视觉 + 通知 Controller 重建 Small)
|
||
var bigBtn = FindBigButton(bigType);
|
||
if (bigBtn == null || bigBtn.Button == null) return;
|
||
bigBtn.Button.onClick.Invoke();
|
||
|
||
// 2) 触发 Small 按钮(如果该 Big 下有 Small 子分类)
|
||
if (hasSmall)
|
||
{
|
||
var smallBtn = FindSmallButton(smallType);
|
||
if (smallBtn == null || smallBtn.Button == null) return;
|
||
smallBtn.Button.onClick.Invoke();
|
||
}
|
||
|
||
// 3) 在已经刷新好的 _wikiListItems 里点中目标 wikiId,并把它滚到可视区
|
||
for (int i = 0; i < _wikiListItems.Count; i++)
|
||
{
|
||
var it = _wikiListItems[i];
|
||
if (it == null || it.Button == null) continue;
|
||
if (it.WikiItemId != wikiId) continue;
|
||
it.Button.onClick.Invoke();
|
||
ScrollWikiListItemIntoView(it);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 把指定的 WikiListItem 滚到 ScrollRect 的可视区中央。
|
||
// ScrollRect 没在 prefab 上单独 expose,运行时从 WikiListContent 沿父链找第一个。
|
||
// 注意:列表是 RefreshWikiList 后立刻调用此方法,需要先 ForceRebuildLayout 让 Content
|
||
// 的高度和子节点位置都已经确定,否则 anchoredPosition.y 可能还是 0。
|
||
private void ScrollWikiListItemIntoView(WikiListItem item)
|
||
{
|
||
if (item == null) return;
|
||
var content = WikiListContent as RectTransform;
|
||
if (content == null) return;
|
||
|
||
var scroll = FindParentScrollRect(content);
|
||
if (scroll == null) return;
|
||
var viewport = scroll.viewport != null ? scroll.viewport : content.parent as RectTransform;
|
||
if (viewport == null) return;
|
||
|
||
Canvas.ForceUpdateCanvases();
|
||
LayoutRebuilder.ForceRebuildLayoutImmediate(content);
|
||
|
||
var target = item.transform as RectTransform;
|
||
if (target == null) return;
|
||
|
||
float contentHeight = content.rect.height;
|
||
float viewportHeight = viewport.rect.height;
|
||
// Content 比 Viewport 矮的时候不需要滚
|
||
if (contentHeight <= viewportHeight) return;
|
||
|
||
// target 在 content 局部空间下的中心 Y(content pivot 默认是 (0.5,1),y 向下为负)
|
||
// 用 target.localPosition.y 即可(已是相对 content)
|
||
float targetY = -target.localPosition.y; // 转成"距 content 顶部"的正向距离
|
||
float scrollableRange = contentHeight - viewportHeight;
|
||
// verticalNormalizedPosition: 1 = 顶部,0 = 底部
|
||
// 让 target 中心对齐 viewport 中心:顶部需要让出 (targetY - viewportHeight/2)
|
||
float topOffset = Mathf.Clamp(targetY - viewportHeight * 0.5f, 0f, scrollableRange);
|
||
scroll.verticalNormalizedPosition = 1f - topOffset / scrollableRange;
|
||
}
|
||
|
||
private static ScrollRect FindParentScrollRect(Transform start)
|
||
{
|
||
var t = start;
|
||
while (t != null)
|
||
{
|
||
var sr = t.GetComponent<ScrollRect>();
|
||
if (sr != null) return sr;
|
||
t = t.parent;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private bool TryResolveJumpRoute(WikiItem target, out WikiType bigType, out WikiType smallType, out bool hasSmall)
|
||
{
|
||
bigType = default;
|
||
smallType = default;
|
||
hasSmall = false;
|
||
if (SelectGroups == null) return false;
|
||
|
||
// target.Types 的顺序里,BigType* 总是先于 SmallType*;遍历 SelectGroups 找可命中的组合
|
||
for (int g = 0; g < SelectGroups.Count; g++)
|
||
{
|
||
var grp = SelectGroups[g];
|
||
if (grp == null) continue;
|
||
if (!target.Types.Contains(grp.BigType)) continue;
|
||
|
||
if (grp.SmallTypes == null || grp.SmallTypes.Count == 0)
|
||
{
|
||
bigType = grp.BigType;
|
||
hasSmall = false;
|
||
return true;
|
||
}
|
||
|
||
for (int s = 0; s < grp.SmallTypes.Count; s++)
|
||
{
|
||
if (!target.Types.Contains(grp.SmallTypes[s])) continue;
|
||
bigType = grp.BigType;
|
||
smallType = grp.SmallTypes[s];
|
||
hasSmall = true;
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private BigSelectButton FindBigButton(WikiType bigType)
|
||
{
|
||
for (int i = 0; i < _bigSelectButtons.Count; i++)
|
||
{
|
||
var b = _bigSelectButtons[i];
|
||
if (b == null) continue;
|
||
if (b.WikiType.Equals(bigType)) return b;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private SmallSelectButton FindSmallButton(WikiType smallType)
|
||
{
|
||
for (int i = 0; i < _smallSelectButtons.Count; i++)
|
||
{
|
||
var b = _smallSelectButtons[i];
|
||
if (b == null) continue;
|
||
if (b.WikiType.Equals(smallType)) return b;
|
||
}
|
||
return null;
|
||
}
|
||
}
|
||
}
|