TH1/Unity/Assets/Scripts/TH1_UI/HintUI/HintWindowController.cs
2026-04-09 20:08:47 +08:00

153 lines
5.9 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.Collections.Generic;
using TH1_UI.HintUI;
using UI.HintUI;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
[RequireComponent(typeof(RectTransform))]
public class HintWindowController : MonoBehaviour
{
[SerializeField] private TextMeshProUGUI _text;
[SerializeField] private Vector2 _padding = new Vector2(16, 10);
[SerializeField] private Vector2 _anchorOffset = new Vector2(16, 16);
[Header("SubHint")]
[SerializeField] private GameObject _line;
[SerializeField] private RectTransform _subHintArea;
[SerializeField] private SubHintItemMono _subHintItemPrefab;
private List<SubHintItemMono> _subHintItems = new List<SubHintItemMono>();
private RectTransform _selfRt;
private Canvas _rootCanvas;
private Camera _uiCamera;
private void Awake()
{
_selfRt = (RectTransform)transform;
_rootCanvas = GetComponentInParent<Canvas>();
_uiCamera = _rootCanvas.worldCamera;
// 兜底找 Text
if (_text == null)
_text = GetComponentInChildren<TextMeshProUGUI>(true);
if (_text == null)
Debug.LogError("[HintWindowController] 找不到 TextMeshProUGUI", this);
}
/// <summary>
/// 设置 SubHintArea 的内容。传入 null 或空列表则隐藏 Line 和 SubHintArea。
/// 每个 HintDataProvider 对应一个 SubHintItemMono。
/// </summary>
public void SetSubHint(List<HintDataProvider> providers)
{
bool show = providers != null && providers.Count > 0;
if (_line != null) _line.SetActive(show);
if (_subHintArea != null) _subHintArea.gameObject.SetActive(show);
if (!show) return;
// 确保有足够的 SubHintItemMono 子对象
while (_subHintItems.Count < providers.Count && _subHintArea != null)
{
SubHintItemMono newItem;
if (_subHintItemPrefab != null)
{
newItem = Instantiate(_subHintItemPrefab, _subHintArea);
}
else
{
// 没有指定 Prefab 时,动态创建
var go = new GameObject($"SubHintItem_{_subHintItems.Count}", typeof(RectTransform));
go.transform.SetParent(_subHintArea, false);
var tmp = go.AddComponent<TextMeshProUGUI>();
tmp.fontSize = _text != null ? _text.fontSize : 14f;
tmp.color = _text != null ? _text.color : Color.white;
tmp.enableAutoSizing = false;
newItem = go.AddComponent<SubHintItemMono>();
}
_subHintItems.Add(newItem);
}
// 设置数据源并控制显隐
for (int i = 0; i < _subHintItems.Count; i++)
{
if (i < providers.Count)
{
_subHintItems[i].gameObject.SetActive(true);
_subHintItems[i].SetDataProvider(providers[i]);
}
else
{
_subHintItems[i].gameObject.SetActive(false);
}
}
}
public void Present(Vector2 mouseScreenPos)
{
if (_text == null) return;
RefreshSize();
ClampToScreen(mouseScreenPos);
}
public void RefreshSize()
{
_text.ForceMeshUpdate(false, false);
LayoutRebuilder.ForceRebuildLayoutImmediate(_text.rectTransform);
float w = _text.preferredWidth + _padding.x * 2f;
float h = _text.preferredHeight + _padding.y * 2f;
// 如果 SubHintArea 可见,把它的高度也算进去
if (_subHintArea != null && _subHintArea.gameObject.activeSelf)
{
LayoutRebuilder.ForceRebuildLayoutImmediate(_subHintArea);
float lineH = (_line != null && _line.activeSelf) ? ((RectTransform)_line.transform).rect.height : 0f;
h += lineH + _subHintArea.rect.height;
}
_selfRt.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, w);
_selfRt.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, h);
Canvas.ForceUpdateCanvases();
}
private void ClampToScreen(Vector2 mouseScreenPos)
{
// 1. 获取 Canvas 的缩放因子,这是解决打包后问题的关键
float scaleFactor = _rootCanvas.scaleFactor;
if (scaleFactor <= 0) scaleFactor = 1f; // 极端情况下的保护
// 2. 计算提示窗口在屏幕上的实际像素尺寸
float windowW = _selfRt.rect.width * scaleFactor;
float windowH = _selfRt.rect.height * scaleFactor;
// 3. 计算窗口左上角假想的屏幕坐标 (这里逻辑保持不变)
Vector2 desiredTopLeft = mouseScreenPos + _anchorOffset;
// 4. 修正:将假想的左上角位置限制在屏幕范围内
// 使用 Screen.width 和 Screen.height 更直接
float clampedX = Mathf.Clamp(desiredTopLeft.x, 0, Screen.width - windowW);
float clampedY = Mathf.Clamp(desiredTopLeft.y, windowH, Screen.height); // Y坐标从下到上所以下边界是windowH
// 5. 关键修正根据Pivot计算最终实际要设置的屏幕坐标点
// 我们已经得到了窗口内容左上角的正确位置(clampedX, clampedY)。
// 现在需要反推出在这种情况下窗口的Pivot应该在屏幕的哪个坐标。
Vector2 pivot = _selfRt.pivot;
Vector2 pivotScreenPos = new Vector2(
clampedX + pivot.x * windowW,
clampedY - (1 - pivot.y) * windowH
);
// 6. 将计算出的Pivot屏幕坐标转换为Canvas的局部坐标
RectTransform canvasRt = _rootCanvas.GetComponent<RectTransform>();
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvasRt,
pivotScreenPos,
_uiCamera, // 即使_uiCamera为null (Overlay模式),此方法依然有效
out Vector2 localPos);
// 7. 设置最终位置
_selfRt.localPosition = localPos;
_selfRt.SetAsLastSibling();
}
}