2025-08-11 14:04:06 -04:00
|
|
|
|
using System;
|
|
|
|
|
|
using DG.Tweening;
|
2026-03-14 03:13:10 -04:00
|
|
|
|
using SLSUtilities.WwiseAssistance;
|
2025-08-11 14:04:06 -04:00
|
|
|
|
using TMPro;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.UI;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Ichni.Menu.UI
|
|
|
|
|
|
{
|
2026-07-19 16:44:58 -04:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 单条文本提示框。
|
|
|
|
|
|
/// 多条弹窗的 FIFO 顺序由 MessageUIPage 统一管理;本类只负责当前一条消息的布局、
|
|
|
|
|
|
/// 入退场视觉和确认输入,避免与 UIPage 的队列职责重复。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public sealed class MessageBox : MonoBehaviour
|
2025-08-11 14:04:06 -04:00
|
|
|
|
{
|
2026-07-19 16:44:58 -04:00
|
|
|
|
[Header("Base")]
|
|
|
|
|
|
[SerializeField] private CanvasGroup canvasGroup;
|
|
|
|
|
|
[SerializeField] private Button receiveButton;
|
|
|
|
|
|
|
|
|
|
|
|
[Header("Text")]
|
|
|
|
|
|
[SerializeField] private TMP_Text titleText;
|
|
|
|
|
|
[SerializeField] private TMP_Text contentText;
|
|
|
|
|
|
|
|
|
|
|
|
[Header("Layout References")]
|
|
|
|
|
|
[Tooltip("可变高度的最终布局节点。动画期间不会修改它、Title 或 Content 的 RectTransform。")]
|
|
|
|
|
|
[SerializeField] private RectTransform messageBody;
|
|
|
|
|
|
[SerializeField] private RectTransform backgroundVisual;
|
|
|
|
|
|
[SerializeField] private RectMask2D titleTextMask;
|
|
|
|
|
|
[SerializeField] private RectTransform contentLayoutRoot;
|
|
|
|
|
|
[SerializeField] private RectMask2D contentTextMask;
|
|
|
|
|
|
[SerializeField] private RectTransform railRoot;
|
|
|
|
|
|
[SerializeField] private RectTransform leftRail;
|
|
|
|
|
|
[SerializeField] private RectTransform rightRail;
|
|
|
|
|
|
|
|
|
|
|
|
[Header("Content Height Limit")]
|
|
|
|
|
|
[SerializeField, Min(0f)] private float maximumBodyHeight = 600f;
|
|
|
|
|
|
[SerializeField, Min(1f)] private float minimumContentFontSize = 18f;
|
|
|
|
|
|
|
|
|
|
|
|
[Header("Animation")]
|
|
|
|
|
|
[Tooltip("背景与文本 Mask 从中心展开前的延迟;Rail 的默认延迟为 0,因此会先行动。")]
|
|
|
|
|
|
[SerializeField, Min(0f)] private float bodyOpenDelay = 0.1f;
|
|
|
|
|
|
[SerializeField, Min(0f)] private float bodyOpenDuration = 0.5f;
|
|
|
|
|
|
[SerializeField, Min(0f)] private float railOpenDelay;
|
|
|
|
|
|
[SerializeField, Min(0f)] private float railOpenDuration = 0.5f;
|
|
|
|
|
|
[SerializeField, Min(0f)] private float bodyCloseDelay;
|
|
|
|
|
|
[SerializeField, Min(0f)] private float bodyCloseDuration = 0.5f;
|
|
|
|
|
|
[SerializeField, Min(0f)] private float railCloseDelay = 0.1f;
|
|
|
|
|
|
[SerializeField, Min(0f)] private float railCloseDuration = 0.5f;
|
|
|
|
|
|
[SerializeField, Min(0f)] private float closedMaskHorizontalPadding = 520f;
|
|
|
|
|
|
[SerializeField] private Ease bodyEase = Ease.OutCubic;
|
|
|
|
|
|
[SerializeField] private Ease railEase = Ease.OutQuart;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 退场动画结束后触发。MessageUIPage 订阅它以销毁当前实例并继续 FIFO 队列。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public event Action<MessageBox> Closed;
|
|
|
|
|
|
|
|
|
|
|
|
private Sequence transitionSequence;
|
|
|
|
|
|
private CanvasGroup leftRailCanvasGroup;
|
|
|
|
|
|
private CanvasGroup rightRailCanvasGroup;
|
|
|
|
|
|
private Vector2 backgroundOpenSizeDelta;
|
|
|
|
|
|
private Vector2 leftRailOpenPosition;
|
|
|
|
|
|
private Vector2 rightRailOpenPosition;
|
|
|
|
|
|
private float contentMaximumFontSize;
|
|
|
|
|
|
|
|
|
|
|
|
private void Awake()
|
2025-08-11 14:04:06 -04:00
|
|
|
|
{
|
2026-07-19 16:44:58 -04:00
|
|
|
|
backgroundOpenSizeDelta = backgroundVisual.sizeDelta;
|
|
|
|
|
|
leftRailOpenPosition = leftRail.anchoredPosition;
|
|
|
|
|
|
rightRailOpenPosition = rightRail.anchoredPosition;
|
|
|
|
|
|
leftRailCanvasGroup = leftRail.GetComponent<CanvasGroup>();
|
|
|
|
|
|
rightRailCanvasGroup = rightRail.GetComponent<CanvasGroup>();
|
|
|
|
|
|
contentMaximumFontSize = contentText.fontSize;
|
|
|
|
|
|
|
|
|
|
|
|
receiveButton.onClick.AddListener(HandleReceiveButtonClicked);
|
2025-08-11 14:04:06 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-19 16:44:58 -04:00
|
|
|
|
private void OnDestroy()
|
2025-08-11 14:04:06 -04:00
|
|
|
|
{
|
2026-07-19 16:44:58 -04:00
|
|
|
|
transitionSequence?.Kill();
|
|
|
|
|
|
receiveButton.onClick.RemoveListener(HandleReceiveButtonClicked);
|
2025-08-11 14:04:06 -04:00
|
|
|
|
}
|
2026-07-19 16:44:58 -04:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 写入唯一一条待显示文本,并播放入场动画。
|
|
|
|
|
|
/// 多条提示请分别提交给 MessageUIPage,而不要在一个 MessageBox 中追加消息。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void SetUp(string title, string content)
|
2025-08-11 14:04:06 -04:00
|
|
|
|
{
|
2026-07-19 16:44:58 -04:00
|
|
|
|
titleText.text = title ?? string.Empty;
|
|
|
|
|
|
contentText.text = content ?? string.Empty;
|
|
|
|
|
|
|
|
|
|
|
|
RefreshLayoutAndFitContentText();
|
|
|
|
|
|
Open();
|
2025-08-11 14:04:06 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-19 16:44:58 -04:00
|
|
|
|
private void HandleReceiveButtonClicked()
|
2025-08-11 14:04:06 -04:00
|
|
|
|
{
|
2026-07-19 16:44:58 -04:00
|
|
|
|
AudioManager.Post(AK.EVENTS.CONFIRM);
|
|
|
|
|
|
Close();
|
2025-08-11 14:04:06 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-19 16:44:58 -04:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 使用最终宽度计算文字换行与内容高度。正文超过 maximumBodyHeight 时,
|
|
|
|
|
|
/// 通过二分查找缩小至可容纳的最大字号;不会引入滚动或截断内容。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void RefreshLayoutAndFitContentText()
|
2025-08-11 14:04:06 -04:00
|
|
|
|
{
|
2026-07-19 16:44:58 -04:00
|
|
|
|
SetOpenVisualState();
|
|
|
|
|
|
ForceRebuildLayout();
|
|
|
|
|
|
|
|
|
|
|
|
if (maximumBodyHeight <= 0f)
|
2025-08-11 14:04:06 -04:00
|
|
|
|
{
|
2026-07-19 16:44:58 -04:00
|
|
|
|
return;
|
2025-08-11 14:04:06 -04:00
|
|
|
|
}
|
2026-07-19 16:44:58 -04:00
|
|
|
|
|
|
|
|
|
|
contentText.enableAutoSizing = false;
|
|
|
|
|
|
contentText.fontSize = contentMaximumFontSize;
|
|
|
|
|
|
ForceRebuildLayout();
|
|
|
|
|
|
|
|
|
|
|
|
if (messageBody.rect.height <= maximumBodyHeight)
|
2025-08-11 14:04:06 -04:00
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-07-19 16:44:58 -04:00
|
|
|
|
|
|
|
|
|
|
float minimumFontSize = Mathf.Min(minimumContentFontSize, contentMaximumFontSize);
|
|
|
|
|
|
float lowerBound = minimumFontSize;
|
|
|
|
|
|
float upperBound = contentMaximumFontSize;
|
|
|
|
|
|
float bestFontSize = minimumFontSize;
|
|
|
|
|
|
|
|
|
|
|
|
// 字号与内容高度单调相关;六次二分已足够平滑且只在弹窗创建时执行。
|
|
|
|
|
|
for (int iteration = 0; iteration < 6; iteration++)
|
|
|
|
|
|
{
|
|
|
|
|
|
float candidate = (lowerBound + upperBound) * 0.5f;
|
|
|
|
|
|
contentText.fontSize = candidate;
|
|
|
|
|
|
ForceRebuildLayout();
|
|
|
|
|
|
|
|
|
|
|
|
if (messageBody.rect.height <= maximumBodyHeight)
|
|
|
|
|
|
{
|
|
|
|
|
|
bestFontSize = candidate;
|
|
|
|
|
|
lowerBound = candidate;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
upperBound = candidate;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
contentText.fontSize = bestFontSize;
|
|
|
|
|
|
ForceRebuildLayout();
|
|
|
|
|
|
|
|
|
|
|
|
if (messageBody.rect.height > maximumBodyHeight)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning(
|
|
|
|
|
|
$"[MessageBox] 正文在最小字号 {minimumFontSize:F1} 下仍超过最大高度 {maximumBodyHeight:F1};" +
|
|
|
|
|
|
"已保留完整内容并允许消息框继续增高。",
|
|
|
|
|
|
this);
|
|
|
|
|
|
}
|
2025-08-11 14:04:06 -04:00
|
|
|
|
}
|
2026-07-19 16:44:58 -04:00
|
|
|
|
|
|
|
|
|
|
private void ForceRebuildLayout()
|
2025-08-11 14:04:06 -04:00
|
|
|
|
{
|
2026-07-19 16:44:58 -04:00
|
|
|
|
Canvas.ForceUpdateCanvases();
|
|
|
|
|
|
LayoutRebuilder.ForceRebuildLayoutImmediate(contentLayoutRoot);
|
|
|
|
|
|
LayoutRebuilder.ForceRebuildLayoutImmediate(messageBody);
|
|
|
|
|
|
Canvas.ForceUpdateCanvases();
|
2025-08-11 14:04:06 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-19 16:44:58 -04:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 恢复最终视觉尺寸以完成布局。这里不会改变 MessageBox、Title 或 Content 的尺寸,
|
|
|
|
|
|
/// 因此 TMP 在展开和收拢全过程都不会重新换行。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void SetOpenVisualState()
|
|
|
|
|
|
{
|
|
|
|
|
|
backgroundVisual.sizeDelta = backgroundOpenSizeDelta;
|
|
|
|
|
|
titleTextMask.padding = Vector4.zero;
|
|
|
|
|
|
contentTextMask.padding = Vector4.zero;
|
|
|
|
|
|
leftRail.anchoredPosition = leftRailOpenPosition;
|
|
|
|
|
|
rightRail.anchoredPosition = rightRailOpenPosition;
|
|
|
|
|
|
leftRailCanvasGroup.alpha = 1f;
|
|
|
|
|
|
rightRailCanvasGroup.alpha = 1f;
|
|
|
|
|
|
}
|
2025-08-11 14:04:06 -04:00
|
|
|
|
|
2026-07-19 16:44:58 -04:00
|
|
|
|
private void SetClosedVisualState()
|
2025-08-11 14:04:06 -04:00
|
|
|
|
{
|
2026-07-19 16:44:58 -04:00
|
|
|
|
backgroundVisual.sizeDelta = GetClosedBackgroundSizeDelta();
|
|
|
|
|
|
titleTextMask.padding = ClosedMaskPadding;
|
|
|
|
|
|
contentTextMask.padding = ClosedMaskPadding;
|
|
|
|
|
|
|
|
|
|
|
|
float halfRootWidth = railRoot.rect.width * 0.5f;
|
|
|
|
|
|
leftRail.anchoredPosition = new Vector2(halfRootWidth, leftRailOpenPosition.y);
|
|
|
|
|
|
rightRail.anchoredPosition = new Vector2(-halfRootWidth, rightRailOpenPosition.y);
|
|
|
|
|
|
leftRailCanvasGroup.alpha = 0f;
|
|
|
|
|
|
rightRailCanvasGroup.alpha = 0f;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private Vector4 ClosedMaskPadding =>
|
|
|
|
|
|
new(closedMaskHorizontalPadding, 0f, closedMaskHorizontalPadding, 0f);
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Background 使用左右 Stretch 锚点。以最终实际宽度抵消它的 SizeDelta X,
|
|
|
|
|
|
/// 可将图像视觉收拢到中心而不改变 MessageBox 的布局宽度。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private Vector2 GetClosedBackgroundSizeDelta() =>
|
|
|
|
|
|
new(backgroundOpenSizeDelta.x - backgroundVisual.rect.width, backgroundOpenSizeDelta.y);
|
|
|
|
|
|
|
|
|
|
|
|
private static Tween CreateMaskPaddingTween(RectMask2D mask, Vector4 targetPadding, float duration, Ease ease) =>
|
|
|
|
|
|
DOTween.To(() => mask.padding, value => mask.padding = value, targetPadding, duration).SetEase(ease);
|
|
|
|
|
|
|
|
|
|
|
|
private static void AddRailTransition(
|
|
|
|
|
|
Sequence sequence,
|
|
|
|
|
|
RectTransform rail,
|
|
|
|
|
|
CanvasGroup railCanvasGroup,
|
|
|
|
|
|
Vector2 targetPosition,
|
|
|
|
|
|
float targetAlpha,
|
|
|
|
|
|
float delay,
|
|
|
|
|
|
float duration,
|
|
|
|
|
|
Ease positionEase)
|
|
|
|
|
|
{
|
|
|
|
|
|
sequence.Insert(delay, rail.DOAnchorPos(targetPosition, duration).SetEase(positionEase));
|
|
|
|
|
|
sequence.Insert(delay, railCanvasGroup.DOFade(targetAlpha, duration).SetEase(Ease.Linear));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 只展开 Background 与两个文字 Mask;MessageBox、标题、正文均已是最终布局尺寸。
|
|
|
|
|
|
/// 每个 Sequence 都先 Pause,再在结尾显式调用 Play,符合项目的 DOTween 使用约定。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void Open()
|
|
|
|
|
|
{
|
|
|
|
|
|
transitionSequence?.Kill();
|
|
|
|
|
|
|
2025-08-11 14:04:06 -04:00
|
|
|
|
canvasGroup.gameObject.SetActive(true);
|
2026-07-19 16:44:58 -04:00
|
|
|
|
canvasGroup.alpha = 1f;
|
|
|
|
|
|
canvasGroup.interactable = false;
|
|
|
|
|
|
canvasGroup.blocksRaycasts = false;
|
|
|
|
|
|
SetClosedVisualState();
|
|
|
|
|
|
|
|
|
|
|
|
transitionSequence = DOTween.Sequence().SetAutoKill(true).Pause();
|
|
|
|
|
|
transitionSequence.Insert(
|
|
|
|
|
|
bodyOpenDelay,
|
|
|
|
|
|
backgroundVisual.DOSizeDelta(backgroundOpenSizeDelta, bodyOpenDuration).SetEase(bodyEase));
|
|
|
|
|
|
transitionSequence.Insert(
|
|
|
|
|
|
bodyOpenDelay,
|
|
|
|
|
|
CreateMaskPaddingTween(titleTextMask, Vector4.zero, bodyOpenDuration, bodyEase));
|
|
|
|
|
|
transitionSequence.Insert(
|
|
|
|
|
|
bodyOpenDelay,
|
|
|
|
|
|
CreateMaskPaddingTween(contentTextMask, Vector4.zero, bodyOpenDuration, bodyEase));
|
|
|
|
|
|
AddRailTransition(
|
|
|
|
|
|
transitionSequence,
|
|
|
|
|
|
leftRail,
|
|
|
|
|
|
leftRailCanvasGroup,
|
|
|
|
|
|
leftRailOpenPosition,
|
|
|
|
|
|
1f,
|
|
|
|
|
|
railOpenDelay,
|
|
|
|
|
|
railOpenDuration,
|
|
|
|
|
|
railEase);
|
|
|
|
|
|
AddRailTransition(
|
|
|
|
|
|
transitionSequence,
|
|
|
|
|
|
rightRail,
|
|
|
|
|
|
rightRailCanvasGroup,
|
|
|
|
|
|
rightRailOpenPosition,
|
|
|
|
|
|
1f,
|
|
|
|
|
|
railOpenDelay,
|
|
|
|
|
|
railOpenDuration,
|
|
|
|
|
|
railEase);
|
|
|
|
|
|
|
|
|
|
|
|
transitionSequence.OnComplete(() =>
|
2025-08-11 14:04:06 -04:00
|
|
|
|
{
|
|
|
|
|
|
canvasGroup.interactable = true;
|
|
|
|
|
|
canvasGroup.blocksRaycasts = true;
|
|
|
|
|
|
});
|
2026-07-19 16:44:58 -04:00
|
|
|
|
transitionSequence.Play();
|
2025-08-11 14:04:06 -04:00
|
|
|
|
}
|
2026-07-19 16:44:58 -04:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 反向收拢 Background、文字 Mask 和两侧 Rail;完成后通知 MessageUIPage 显示下一项。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void Close()
|
2025-08-11 14:04:06 -04:00
|
|
|
|
{
|
2026-07-19 16:44:58 -04:00
|
|
|
|
transitionSequence?.Kill();
|
2025-08-11 14:04:06 -04:00
|
|
|
|
canvasGroup.interactable = false;
|
|
|
|
|
|
canvasGroup.blocksRaycasts = false;
|
2026-07-19 16:44:58 -04:00
|
|
|
|
|
|
|
|
|
|
float halfRootWidth = railRoot.rect.width * 0.5f;
|
|
|
|
|
|
transitionSequence = DOTween.Sequence().SetAutoKill(true).Pause();
|
|
|
|
|
|
transitionSequence.Insert(
|
|
|
|
|
|
bodyCloseDelay,
|
|
|
|
|
|
backgroundVisual.DOSizeDelta(GetClosedBackgroundSizeDelta(), bodyCloseDuration).SetEase(Ease.InCubic));
|
|
|
|
|
|
transitionSequence.Insert(
|
|
|
|
|
|
bodyCloseDelay,
|
|
|
|
|
|
CreateMaskPaddingTween(titleTextMask, ClosedMaskPadding, bodyCloseDuration, Ease.InCubic));
|
|
|
|
|
|
transitionSequence.Insert(
|
|
|
|
|
|
bodyCloseDelay,
|
|
|
|
|
|
CreateMaskPaddingTween(contentTextMask, ClosedMaskPadding, bodyCloseDuration, Ease.InCubic));
|
|
|
|
|
|
AddRailTransition(
|
|
|
|
|
|
transitionSequence,
|
|
|
|
|
|
leftRail,
|
|
|
|
|
|
leftRailCanvasGroup,
|
|
|
|
|
|
new Vector2(halfRootWidth, leftRailOpenPosition.y),
|
|
|
|
|
|
0f,
|
|
|
|
|
|
railCloseDelay,
|
|
|
|
|
|
railCloseDuration,
|
|
|
|
|
|
Ease.InQuart);
|
|
|
|
|
|
AddRailTransition(
|
|
|
|
|
|
transitionSequence,
|
|
|
|
|
|
rightRail,
|
|
|
|
|
|
rightRailCanvasGroup,
|
|
|
|
|
|
new Vector2(-halfRootWidth, rightRailOpenPosition.y),
|
|
|
|
|
|
0f,
|
|
|
|
|
|
railCloseDelay,
|
|
|
|
|
|
railCloseDuration,
|
|
|
|
|
|
Ease.InQuart);
|
|
|
|
|
|
|
|
|
|
|
|
transitionSequence.OnComplete(() =>
|
2025-08-11 14:04:06 -04:00
|
|
|
|
{
|
|
|
|
|
|
canvasGroup.gameObject.SetActive(false);
|
2026-07-19 16:44:58 -04:00
|
|
|
|
Closed?.Invoke(this);
|
2025-08-11 14:04:06 -04:00
|
|
|
|
});
|
2026-07-19 16:44:58 -04:00
|
|
|
|
transitionSequence.Play();
|
2025-08-11 14:04:06 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-19 16:44:58 -04:00
|
|
|
|
}
|