153 lines
5.9 KiB
C#
153 lines
5.9 KiB
C#
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();
|
||
}
|
||
}
|